| """ |
| 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 |
| |
| sys.path.append(os.path.dirname(os.path.abspath(__file__))) |
|
|
| |
| from logging_config import setup_logging, get_logger |
|
|
| |
| setup_logging() |
| logger = get_logger(__name__) |
|
|
| |
| 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. |
| """ |
| |
| print("Starting AegisLM Red Team API...") |
| |
| try: |
| |
| if not settings.DEBUG: |
| if not hasattr(settings, "STARTUP_VALIDATION") or settings.DEBUG: |
| |
| print("[!] Production validation module not available, skipping...") |
| else: |
| print("[DEBUG] Debug mode enabled - skipping production validation") |
| |
| |
| try: |
| await init_db() |
| print("✅ Database initialized successfully") |
| |
| |
| 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)") |
| |
| |
| print("[SKIP] Skipping redundant health checks (production validation already passed)") |
| |
| |
| 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 |
| |
| |
| 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)}") |
|
|
|
|
| |
| 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 |
| ) |
|
|
| |
| |
| app.add_middleware(GlobalErrorHandler) |
| app.add_middleware(InputValidationMiddleware) |
| app.add_middleware(SecurityHeadersMiddleware) |
| app.add_middleware(RequestLoggingMiddleware) |
| app.add_middleware(APIKeyAuthMiddleware) |
| app.add_middleware(RateLimitMiddleware) |
|
|
| |
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=settings.BACKEND_CORS_ORIGINS, |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| 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"] |
| ) |
|
|
|
|
| |
| app.include_router( |
| websocket_routes, |
| tags=["websocket"] |
| ) |
|
|
|
|
| |
| @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() |
|
|
|
|
| |
| @app.get("/health", response_model=HealthCheckResponse, tags=["health"]) |
| async def health_check(): |
| """ |
| Comprehensive health check endpoint. |
| |
| Returns: |
| HealthCheckResponse: Health status of all services |
| """ |
| |
| db_healthy = await check_db_health() |
| redis_healthy = await check_redis_health() |
| |
| |
| worker_healthy = False |
| try: |
| from workers.celery_worker import celery_app |
| |
| inspect = celery_app.control.inspect() |
| active_tasks = inspect.active() |
| worker_healthy = active_tasks is not None |
| except Exception: |
| worker_healthy = False |
| |
| |
| overall_status = "healthy" if db_healthy and redis_healthy else "unhealthy" |
| |
| |
| services = { |
| "database": "healthy" if db_healthy else "unhealthy", |
| "redis": "healthy" if redis_healthy else "unhealthy", |
| "worker": "healthy" if worker_healthy else "unhealthy", |
| "api": "healthy" |
| } |
| |
| |
| uptime_seconds = int(time.time() - app.state.start_time) if hasattr(app.state, "start_time") else 0 |
| |
| |
| 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 |
| ) |
|
|
|
|
| |
| @app.get("/metrics", tags=["monitoring"]) |
| async def metrics(): |
| """ |
| Basic metrics endpoint. |
| |
| Returns: |
| dict: Application metrics |
| """ |
| |
| |
| 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 |
| } |
|
|
|
|
| |
| @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__) |
| |
| |
| module_logger.error(f"Validation error occurred: {exc.errors()}") |
| |
| |
| cleaned_errors = [] |
| for error in exc.errors(): |
| cleaned_error = error.copy() |
| |
| 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__) |
| |
| |
| 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() |
| ) |
|
|
|
|
| |
| @app.on_event("startup") |
| async def startup_event(): |
| """ |
| Set application start time. |
| """ |
| app.state.start_time = time.time() |
|
|
|
|
| |
| @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 |
| |
| |
| 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 |
| } |
|
|
|
|
| |
| 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" |
| ) |
|
|