# app/main.py # Lojiz Platform + Aida AI - Graph-Based Architecture (v1 Primary) from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware from app.middleware import limiter, rate_limit_exceeded_handler from contextlib import asynccontextmanager import logging import os logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # ============================================================ # SENTRY INTEGRATION - Error Monitoring # ============================================================ import sentry_sdk from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.starlette import StarletteIntegration def init_sentry(): """Initialize Sentry for production error monitoring""" sentry_dsn = os.getenv("SENTRY_DSN", "") environment = os.getenv("ENVIRONMENT", "development") if sentry_dsn and environment == "production": sentry_sdk.init( dsn=sentry_dsn, environment=environment, integrations=[ FastApiIntegration(transaction_style="endpoint"), StarletteIntegration(transaction_style="endpoint"), ], # Performance monitoring traces_sample_rate=0.2, # 20% of transactions # Error sampling profiles_sample_rate=0.1, # 10% of profiled transactions # Don't send PII send_default_pii=False, # Release version release=os.getenv("APP_VERSION", "1.0.0"), ) logger.info("✅ Sentry initialized for production monitoring") elif sentry_dsn: logger.info(f"⚠️ Sentry DSN set but environment is {environment} - not initializing") else: logger.info("ℹ️ Sentry DSN not configured - error monitoring disabled") # Initialize Sentry before app startup init_sentry() # CORE IMPORTS try: from app.config import settings from app.database import connect_db, disconnect_db, ensure_indexes as ensure_auth_indexes, ensure_review_indexes from app.routes import auth except ImportError as e: logger.error(f"Core import error: {e}") raise try: from app.core.exceptions import AuthException except ImportError: AuthException = Exception # ============================================================ # AI IMPORTS - GRAPH-BASED ARCHITECTURE # ============================================================ try: from app.ai.routes.chat import router as ai_chat_router from app.ai.config import ( validate_ai_startup, check_redis_health, check_qdrant_health, redis_client, qdrant_client, ) from app.ai.memory.redis_context_memory import get_memory_manager from app.ml.models.ml_listing_extractor import get_ml_extractor logger.info("✅ Graph-based AI architecture loaded") except ImportError as e: logger.error(f"AI import error: {e}") raise from app.models.listing import ensure_listing_indexes # ENVIRONMENT environment = os.getenv("ENVIRONMENT", "development") is_production = environment == "production" # LIFESPAN @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan - startup and shutdown""" logger.info("=" * 70) logger.info("🚀 Starting Lojiz Platform + Aida AI") logger.info(" Architecture: Graph-Based (State Machine + Validation)") logger.info(" Primary Endpoint: /ai/v1 (Graph-Based)") logger.info(" Fallback Endpoint: /ai/v2 (Legacy)") logger.info("=" * 70) # STARTUP try: logger.info("Connecting to MongoDB...") await connect_db() await ensure_auth_indexes() await ensure_listing_indexes() await ensure_review_indexes() # Import and create chat indexes from app.database import ensure_chat_indexes await ensure_chat_indexes() logger.info("✅ MongoDB connected and indexed") except Exception as e: logger.critical(f"❌ MongoDB connection failed - aborting startup: {e}") raise try: logger.info("Connecting to Redis...") if redis_client: await redis_client.ping() logger.info("✅ Redis connected") else: logger.warning("⚠️ Redis not available (optional)") except Exception as e: logger.warning(f"⚠️ Redis connection failed (continuing without): {e}") try: logger.info("Connecting to Qdrant...") if qdrant_client: await qdrant_client.get_collections() logger.info("✅ Qdrant connected") else: logger.warning("⚠️ Qdrant not available (optional)") except Exception as e: logger.warning(f"⚠️ Qdrant connection failed (continuing without): {e}") try: logger.info("Validating AI components...") ai_checks = await validate_ai_startup() logger.info("✅ AI components validated") except Exception as e: logger.warning(f"⚠️ AI validation warning: {e}") try: logger.info("Initializing ML Extractor...") ml = get_ml_extractor() logger.info("✅ ML Extractor ready") except Exception as e: logger.warning(f"⚠️ ML Extractor initialization warning: {e}") try: logger.info("Initializing Memory Manager...") manager = get_memory_manager() logger.info("✅ Memory Manager ready") except Exception as e: logger.warning(f"⚠️ Memory Manager initialization warning: {e}") # Initialize Distributed Chat (Redis Pub/Sub) try: from app.routes.websocket_chat import chat_manager await chat_manager.initialize() logger.info("✅ Redis Pub/Sub for Chat initialized") except Exception as e: logger.warning(f"⚠️ Chat manager init failed: {e}") logger.info("=" * 70) logger.info("✅ APPLICATION READY - Graph-Based Architecture Active!") logger.info("=" * 70) yield # SHUTDOWN logger.info("=" * 70) logger.info("🛑 Shutting down Lojiz Platform + Aida AI") logger.info("=" * 70) try: try: ml = get_ml_extractor() ml.currency_mgr.clear_cache() logger.info("✅ ML caches cleared") except Exception as e: logger.debug(f"ML cache clear skipped: {e}") from app.database import disconnect_db await disconnect_db() logger.info("✅ MongoDB disconnected") if redis_client: await redis_client.close() logger.info("✅ Redis closed") logger.info("✅ Shutdown complete") except Exception as e: logger.warning(f"⚠️ Shutdown warning: {e}") # FASTAPI SETUP app = FastAPI( title="Lojiz Platform + Aida AI", description="Real-estate platform with conversational AI assistant (Graph-Based Architecture)", version="2.0.0", lifespan=lifespan, ) # RATE LIMITING app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) # CORS - Allow all localhost ports in development if is_production: cors_origins = [ "https://lojiz.onrender.com", "https://lojiz.com", "https://www.lojiz.com", ] cors_origin_regex = None else: # Development - allow any localhost port (Flutter uses random ports like 50421) cors_origins = [ "http://localhost", "http://127.0.0.1", ] # Regex to match localhost with any port cors_origin_regex = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$" app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_origin_regex=cors_origin_regex, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["*"], max_age=600, ) # EXCEPTION HANDLERS @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): logger.error(f"Validation error: {exc}") errors = [] for error in exc.errors(): field = ".".join(str(loc) for loc in error["loc"][1:]) errors.append({"field": field, "message": error["msg"]}) return JSONResponse( status_code=400, content={ "success": False, "message": "Validation error. Please check your input.", "error_code": "VALIDATION_ERROR", "errors": errors, }, ) @app.exception_handler(AuthException) async def auth_exception_handler(request: Request, exc: AuthException): # type: ignore logger.warning(f"Auth error [{exc.error_code}]: {exc.message}") response = {"success": False, "message": exc.message, "error_code": exc.error_code} if exc.data: response["data"] = exc.data return JSONResponse(status_code=exc.status_code, content=response) # ROUTERS logger.info("=" * 70) logger.info("Registering routers...") logger.info("=" * 70) # Authentication app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"]) # AI CHAT ROUTES (Two distinct systems) # 1. AIDA Chat Screen - Full Agent (/ai/ask) # 2. AIDA DM - Simple Agent (/ai/dm) try: from app.ai.routes.chat import router as ai_chat_router from app.ai.routes.dm import router as ai_dm_router # Register main chat (/ai/ask) app.include_router( ai_chat_router, prefix="/ai", tags=["AIDA AI Chat (Full Agent / Chat Screen)"] ) # Register DM agent (/ai/dm) app.include_router( ai_dm_router, prefix="/ai", tags=["AIDA AI DM (Conversational / Alert Agent)"] ) logger.info("✅ AIDA AI Chat registered at /ai/ask (Graph-Based)") logger.info("✅ AIDA AI DM registered at /ai/dm (One-Shot)") except ImportError as e: logger.error(f"❌ AI Chat/DM import error: {e}") # AIDA Translation try: from app.ai.routes.translate import router as translate_router app.include_router(translate_router, prefix="/api", tags=["AIDA Translate"]) logger.info("✅ AIDA Translation registered at /api/translate") except Exception as e: logger.error(f"❌ AIDA Translation import error: {e}") # ============================================================ # LISTING ROUTERS # ============================================================ from app.routes.listing import router as listing_router from app.routes.media_upload import router as media_router from app.routes.user_public import router as user_public_router from app.routes.websocket_listings import router as ws_router from app.routes.websocket_chat import router as ws_chat_router from app.routes.reviews import router as reviews_router from app.routes.search import router as search_router from app.routes.conversations import router as conversations_router from app.routes.wishlist import router as wishlist_router app.include_router(listing_router, prefix="/api/listings", tags=["Listings"]) app.include_router(media_router, tags=["Media Upload & Analysis"]) app.include_router(user_public_router, prefix="/api/users", tags=["Users"]) app.include_router(ws_router, tags=["WebSocket Listings"]) app.include_router(ws_chat_router, tags=["WebSocket Chat"]) app.include_router(reviews_router, prefix="/api/reviews", tags=["Reviews"]) app.include_router(search_router, prefix="/api/search", tags=["AIDA Search"]) app.include_router(conversations_router, prefix="/api/conversations", tags=["Conversations"]) app.include_router(wishlist_router, prefix="/api/wishlist", tags=["Wishlist"]) logger.info("✅ Conversations router registered at /api/conversations") logger.info("✅ Wishlist router registered at /api/wishlist") # ============================================================ # VOICE ROUTES (AIDA Voice Messaging) # ============================================================ try: from app.routes.voice import router as voice_router from app.routes.voice_call_ws import router as voice_call_ws_router app.include_router(voice_router, prefix="/api", tags=["Voice"]) app.include_router(voice_call_ws_router, tags=["Voice Call WebSocket"]) logger.info("✅ Voice router registered at /api/voice") logger.info("✅ Voice Call WebSocket registered at /ws/voice-call") except ImportError as e: logger.warning(f"⚠️ Voice router not available: {e}") logger.info("=" * 70) logger.info("✅ All routers registered successfully") logger.info("=" * 70) # ENDPOINTS @app.get("/health", tags=["Health"]) async def health_check(): """Health check endpoint""" try: redis_ok = False if redis_client: try: await redis_client.ping() redis_ok = True except Exception: redis_ok = False qdrant_ok = False if qdrant_client: try: await qdrant_client.get_collections() qdrant_ok = True except Exception: qdrant_ok = False try: ml = get_ml_extractor() ml_ok = ml is not None except Exception: ml_ok = False return { "status": "healthy", "service": "Lojiz Platform + Aida AI", "version": "2.0.0", "architecture": "Graph-Based (State Machine + Validation)", "environment": environment, "ai_endpoints": { "primary": "/ai/v1 (Graph-Based)", "fallback": "/ai/v2 (Legacy)", }, "components": { "mongodb": "connected", "redis": "connected" if redis_ok else "disconnected", "qdrant": "connected" if qdrant_ok else "disconnected", "ml": "ready" if ml_ok else "not ready", } } except Exception as e: logger.error(f"Health check failed: {e}") return { "status": "unhealthy", "error": str(e), } @app.get("/", tags=["Root"]) async def root(): """Root endpoint - API information""" return { "name": "AIDA", "tagline": "Your AI Real Estate Agent", "platform": "Lojiz", "version": "2.0.0", "status": "operational", "environment": environment, "endpoints": { "chat": "/ai/ask", "dm": "/ai/dm", "health": "/health", "docs": "/docs", }, "capabilities": [ "Natural language property search", "Intelligent listing creation", "Real-time market insights", "Multi-language support (EN/FR)", "Voice message processing", ], "powered_by": "LangGraph State Machine", "made_with": "FastAPI + MongoDB + Redis", } @app.options("/{full_path:path}", include_in_schema=False) async def options_handler(full_path: str): """Handle CORS preflight requests""" return JSONResponse(status_code=200, content={}) # RUN # To run this application: # Development: uvicorn app.main:app --reload # Production: gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app # HF Spaces: python app.py