ALM-2 / backend /main.py
ACA050's picture
Upload 520 files
2ed8996 verified
Raw
History Blame Contribute Delete
17.1 kB
"""
Main FastAPI application for AegisLM SaaS Backend.
Production-ready application with proper middleware,
routing, error handling, and startup/shutdown events.
"""
import time
import asyncio
import os
import json
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from fastapi import FastAPI, Request, HTTPException, status, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from typing import List
import sys
import os
import uvicorn
# Fix for nested imports (AegisLM SaaS Structure)
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# Initialize logging first
from logging_config import setup_logging, get_logger
# Setup logging
setup_logging()
logger = get_logger(__name__)
# Load environment variables
from dotenv import load_dotenv
load_dotenv()
from core.config import settings
from core.database import init_db, close_db, check_db_health, check_redis_health
from api.routes import (
auth_routes,
evaluation_routes,
results_routes,
analytics_routes,
benchmark_routes,
report_routes,
experiment_routes,
multimodal_routes,
performance_routes,
monitoring_routes,
sqlite_fallback_routes,
audit_routes,
dataset_routes
)
from websocket.routes import websocket as websocket_routes
from middleware.rate_limit import (
RateLimitMiddleware,
APIKeyAuthMiddleware,
SecurityHeadersMiddleware,
RequestLoggingMiddleware
)
from middleware.validation import InputValidationMiddleware
from middleware.error_handler import GlobalErrorHandler
from schemas.response_schema import ErrorResponse, HealthCheckResponse
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Application lifespan manager.
Handles startup and shutdown events.
"""
# Startup
print("Starting AegisLM Red Team API...")
try:
# Skip production startup validation in debug mode
if not settings.DEBUG:
if not hasattr(settings, "STARTUP_VALIDATION") or settings.DEBUG:
# TODO: Re-enable when startup_validation module is available
print("[!] Production validation module not available, skipping...")
else:
print("[DEBUG] Debug mode enabled - skipping production validation")
# Initialize database (skip if not available)
try:
await init_db()
print("✅ Database initialized successfully")
# Setup SQLite fallback if enabled
if settings.ENABLE_SQLITE_FALLBACK:
try:
from core.sqlite_fallback_manager import setup_sqlite_fallback
fallback_setup = await setup_sqlite_fallback()
if fallback_setup:
print("[INFO] SQLite fallback database setup completed")
else:
print("[!] SQLite fallback database setup failed")
except Exception as e:
print(f"[!] SQLite fallback setup skipped: {str(e)}")
except Exception as e:
print(f"[!] Database initialization skipped: {str(e)}")
print(" (This is expected if PostgreSQL is not running)")
# Skip redundant health checks since production validation already verified services
print("[SKIP] Skipping redundant health checks (production validation already passed)")
# Set startup time
app.state.start_time = time.time()
print(f"[INFO] Server running on {settings.API_V1_STR}")
print(f"[INFO] Debug mode: {settings.DEBUG}")
print(f"[INFO] Environment: {'Production' if not settings.DEBUG else 'Development'}")
except Exception as e:
print(f"Startup failed: {str(e)}")
raise
yield
# Shutdown
print("🛑 Shutting down AegisLM Red Team API...")
try:
await close_db()
print("✅ Database connections closed")
except Exception as e:
print(f"❌ Shutdown error: {str(e)}")
# Create FastAPI application
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="Production-ready API for AI red team evaluations and security assessments",
docs_url="/docs" if settings.DEBUG else None,
redoc_url="/redoc" if settings.DEBUG else None,
openapi_url="/openapi.json" if settings.DEBUG else None,
lifespan=lifespan
)
# Include API routes
# Add custom middleware (order matters)
app.add_middleware(GlobalErrorHandler) # Add first to catch all errors
app.add_middleware(InputValidationMiddleware) # Add input validation
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(RequestLoggingMiddleware)
app.add_middleware(APIKeyAuthMiddleware)
app.add_middleware(RateLimitMiddleware)
# Add CORS middleware LAST so it runs FIRST on incoming requests
# This is critical for handling OPTIONS (preflight) requests before auth/rate-limiting
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API routes
app.include_router(
auth_routes.router,
prefix=settings.API_V1_STR,
tags=["authentication"]
)
#
app.include_router(
evaluation_routes.router,
prefix=settings.API_V1_STR,
tags=["evaluations"]
)
app.include_router(
results_routes.router,
prefix=settings.API_V1_STR,
tags=["results"]
)
app.include_router(
analytics_routes.router,
prefix=settings.API_V1_STR,
tags=["analytics"]
)
app.include_router(
benchmark_routes.router,
prefix=settings.API_V1_STR,
tags=["benchmarks"]
)
app.include_router(
report_routes.router,
prefix=settings.API_V1_STR,
tags=["reports"]
)
app.include_router(
experiment_routes.router,
prefix=settings.API_V1_STR,
tags=["experiments"]
)
app.include_router(
multimodal_routes.router,
prefix=settings.API_V1_STR,
tags=["multimodal"]
)
app.include_router(
performance_routes.router,
prefix=settings.API_V1_STR,
tags=["performance"]
)
#
app.include_router(
monitoring_routes.router,
prefix=settings.API_V1_STR,
tags=["monitoring"]
)
app.include_router(
sqlite_fallback_routes.router,
prefix=settings.API_V1_STR,
tags=["sqlite-fallback"]
)
app.include_router(
audit_routes.router,
prefix=settings.API_V1_STR,
tags=["audit"]
)
app.include_router(
dataset_routes.router,
prefix=settings.API_V1_STR,
tags=["datasets"]
)
# Include WebSocket routes
app.include_router(
websocket_routes,
tags=["websocket"]
)
# Root endpoint
@app.get("/", tags=["root"])
async def root():
"""
Root endpoint.
Returns:
dict: API information
"""
return {
"name": settings.APP_NAME,
"version": settings.APP_VERSION,
"status": "running",
"docs_url": "/docs" if settings.DEBUG else None,
"api_v1": settings.API_V1_STR
}
@app.get("/api/v1/ws/health", tags=["health"])
async def ws_health_check():
"""
Health check for WebSocket connections.
Used by the frontend to monitor connection status.
"""
from datetime import datetime, timezone
return {
"status": "healthy",
"websocket": "available",
"timestamp": datetime.now(timezone.utc).isoformat()
}
@app.get("/api/v1/health", tags=["health"])
async def api_health_check():
"""
API version of the health check.
"""
return await health_check()
# Health check endpoint
@app.get("/health", response_model=HealthCheckResponse, tags=["health"])
async def health_check():
"""
Comprehensive health check endpoint.
Returns:
HealthCheckResponse: Health status of all services
"""
# Check database health
db_healthy = await check_db_health()
redis_healthy = await check_redis_health()
# Check worker status (via Celery)
worker_healthy = False
try:
from workers.celery_worker import celery_app
# Check if Celery worker is responsive
inspect = celery_app.control.inspect()
active_tasks = inspect.active()
worker_healthy = active_tasks is not None
except Exception:
worker_healthy = False
# Overall status (only critical services affect overall health)
overall_status = "healthy" if db_healthy and redis_healthy else "unhealthy"
# Service statuses
services = {
"database": "healthy" if db_healthy else "unhealthy",
"redis": "healthy" if redis_healthy else "unhealthy",
"worker": "healthy" if worker_healthy else "unhealthy",
"api": "healthy"
}
# Calculate uptime
uptime_seconds = int(time.time() - app.state.start_time) if hasattr(app.state, "start_time") else 0
# Additional health information
health_info = {
"environment": "production" if not settings.DEBUG else "development",
"learning_enabled": settings.ENABLE_LEARNING,
"judge_enabled": settings.ENABLE_LLM_JUDGE,
"rate_limiting": f"{settings.RATE_LIMIT_PER_MINUTE}/min"
}
return HealthCheckResponse(
status=overall_status,
version=settings.APP_VERSION,
timestamp=datetime.now(timezone.utc).isoformat(),
services=services,
uptime_seconds=uptime_seconds,
**health_info
)
# Metrics endpoint
@app.get("/metrics", tags=["monitoring"])
async def metrics():
"""
Basic metrics endpoint.
Returns:
dict: Application metrics
"""
# This is a basic metrics endpoint
# In production, you might want to use Prometheus or similar
return {
"status": "ok",
"timestamp": datetime.now(timezone.utc).isoformat(),
"uptime_seconds": int(time.time() - app.state.start_time) if hasattr(app.state, "start_time") else 0,
"version": settings.APP_VERSION,
"debug": settings.DEBUG
}
# Exception handlers
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""
Handle HTTP exceptions.
Args:
request: HTTP request
exc: HTTP exception
Returns:
JSONResponse: Error response
"""
from fastapi.responses import JSONResponse
from datetime import datetime, timezone
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(
success=False,
message=exc.detail,
error_code=f"HTTP_{exc.status_code}",
timestamp=datetime.now(timezone.utc).isoformat()
).dict()
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
Handle validation exceptions.
Args:
request: HTTP request
exc: Validation exception
Returns:
JSONResponse: Validation error response
"""
from fastapi.responses import JSONResponse
from datetime import datetime, timezone
from logging_config import get_logger
module_logger = get_logger(__name__)
# Log the validation errors for debugging
module_logger.error(f"Validation error occurred: {exc.errors()}")
# Clean validation errors to remove non-serializable objects
cleaned_errors = []
for error in exc.errors():
cleaned_error = error.copy()
# Remove non-serializable objects from ctx
if 'ctx' in cleaned_error and 'error' in cleaned_error['ctx']:
cleaned_error['ctx']['error'] = str(cleaned_error['ctx']['error'])
cleaned_errors.append(cleaned_error)
return JSONResponse(
status_code=422,
content=ErrorResponse(
success=False,
message="Validation failed",
error_code="VALIDATION_ERROR",
error_details={
"validation_errors": cleaned_errors
},
timestamp=datetime.now(timezone.utc).isoformat()
).dict()
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""
Handle general exceptions.
Args:
request: HTTP request
exc: General exception
Returns:
JSONResponse: Error response
"""
from fastapi.responses import JSONResponse
from datetime import datetime, timezone
from logging_config import get_logger
import traceback
module_logger = get_logger(__name__)
# Log the full exception
error_id = f"INTERNAL_{id(exc)}"
module_logger.error(
f"Unhandled exception [{error_id}]: {str(exc)}\n"
f"Traceback: {traceback.format_exc()}"
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=ErrorResponse(
success=False,
message="An internal server error occurred. Please try again later.",
error_code="INTERNAL_ERROR",
error_details={"error_id": error_id},
timestamp=datetime.now(timezone.utc).isoformat()
).model_dump()
)
# Startup event to set start time
@app.on_event("startup")
async def startup_event():
"""
Set application start time.
"""
app.state.start_time = time.time()
# Additional utility endpoints
@app.get("/api/v1/info", tags=["info"])
async def api_info():
"""
Get API information.
Returns:
dict: API information
"""
return {
"name": settings.APP_NAME,
"version": settings.APP_VERSION,
"api_version": "v1",
"description": "Production-ready API for AI red team evaluations",
"endpoints": {
"authentication": f"{settings.API_V1_STR}/auth",
"evaluations": f"{settings.API_V1_STR}/evaluations",
"results": f"{settings.API_V1_STR}/results",
"benchmarks": f"{settings.API_V1_STR}/benchmarks"
},
"documentation": "/docs" if settings.DEBUG else "Documentation not available in production"
}
async def check_celery_health():
"""Check if Celery workers are active."""
try:
from workers.celery_worker import celery_app
# Use ping() to check worker connectivity
# Timeout after 1 second to avoid blocking
ping_result = celery_app.control.ping(timeout=1.0)
return ping_result is not None and len(ping_result) > 0
except Exception:
return False
@app.get("/api/v1/status", tags=["status"])
async def api_status():
"""
Get API status.
Returns:
dict: API status
"""
db_healthy = await check_db_health()
redis_healthy = await check_redis_health()
celery_healthy = await check_celery_health()
return {
"status": "operational" if db_healthy and redis_healthy and celery_healthy else "degraded",
"services": {
"database": "online" if db_healthy else "offline",
"redis": "online" if redis_healthy else "offline",
"celery": "online" if celery_healthy else "offline"
},
"uptime_seconds": int(time.time() - app.state.start_time) if hasattr(app.state, "start_time") else 0,
"version": settings.APP_VERSION
}
# Development-only endpoints
if settings.DEBUG:
@app.get("/debug/routes", tags=["debug"])
async def debug_routes():
"""
Debug endpoint to list all routes.
Returns:
dict: Route information
"""
routes = []
for route in app.routes:
routes.append({
"path": route.path,
"name": route.name,
"methods": getattr(route, "methods", None),
"include_in_schema": getattr(route, "include_in_schema", None)
})
return {"routes": routes}
@app.get("/debug/config", tags=["debug"])
async def debug_config():
"""
Debug endpoint to show configuration (masked).
Returns:
dict: Configuration information
"""
from core.security import mask_sensitive_data
return {
"app_name": settings.APP_NAME,
"app_version": settings.APP_VERSION,
"debug": settings.DEBUG,
"database_url": mask_sensitive_data(settings.DATABASE_URL),
"redis_url": settings.REDIS_URL,
"cors_origins": settings.BACKEND_CORS_ORIGINS,
"rate_limit_per_minute": settings.RATE_LIMIT_PER_MINUTE
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8001,
reload=settings.DEBUG,
log_level="info" if not settings.DEBUG else "debug"
)