Spaces:
Running
Running
| # 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 | |
| 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 | |
| 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, | |
| }, | |
| ) | |
| 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 | |
| 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), | |
| } | |
| 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", | |
| } | |
| 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 |