AIDA / main.py
destinyebuka's picture
fyp
dfd3a90
# 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