"""FastAPI application — Daily Grubs Rider Dispatch API.""" import logging import os import sys import time from contextlib import asynccontextmanager # Load .env (DB_*, REDIS_*, GOOGLE_MAPS_API_KEY) into the environment early, # before any service reads os.getenv at import time. try: from dotenv import load_dotenv load_dotenv() except Exception: pass from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from starlette.exceptions import HTTPException as StarletteHTTPException from app.core.exception_handlers import ( api_exception_handler, general_exception_handler, http_exception_handler, validation_exception_handler, ) from app.core.exceptions import APIException from app.middleware.request_id import RequestIDMiddleware from app.routes import cache_router, health_router, ml_router, ml_web_router, optimization_router, batch_analytics_router, riders_router # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- from app.core.log_context import RequestIdFilter _log_level = getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO) _handler = logging.StreamHandler(sys.stdout) _handler.addFilter(RequestIdFilter()) logging.basicConfig( level=_log_level, format="%(asctime)s - %(name)s - %(levelname)s - [req:%(request_id)s] - [%(filename)s:%(lineno)d] - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[_handler], ) logger = logging.getLogger(__name__) for _lib in ("httpx", "uvicorn", "uvicorn.error", "uvicorn.access"): logging.getLogger(_lib).setLevel(_log_level) # --------------------------------------------------------------------------- # Lifespan (startup / shutdown) # --------------------------------------------------------------------------- @asynccontextmanager async def lifespan(app: FastAPI): logger.info("[START] Route Optimization API starting up...") # Warm up the delivery-history FAISS store in the background try: from app.services.vector.delivery_history_store import get_delivery_history_store store = get_delivery_history_store() logger.info( f"[DeliveryHistory] FAISS store ready — {store.record_count()} historical records loaded." ) except Exception as e: logger.warning(f"[DeliveryHistory] Store warm-up failed (non-fatal): {e}") # Warm up the customer-coord FAISS store try: from app.services.vector.faiss_customer_store import get_faiss_store cs = get_faiss_store() logger.info(f"[FAISSCoords] Customer coord store ready — {cs.record_count()} records.") except Exception as e: logger.warning(f"[FAISSCoords] Store warm-up failed (non-fatal): {e}") # Log how many historical assignment events we have for analytics try: from app.services.ml.ml_data_collector import get_collector n = get_collector().count_records() logger.info(f"[Analytics] {n} assignment events in the analytics DB.") except Exception as e: logger.warning(f"[Analytics] DB check failed (non-fatal): {e}") # Start the autonomous empirical-ETA sync agent (daemon thread). # It mirrors completed deliveries locally and rebuilds learned ETAs on a # schedule, so the request path never touches Postgres. try: from app.services.routing.delivery_history_service import get_delivery_history_service from app.config.dynamic_config import get_config cfg = get_config() get_delivery_history_service().ensure_background_sync( interval_hours=int(cfg.get("eta_sync_interval_hours", 6)), days=int(cfg.get("eta_history_days", 14)), ) logger.info("[ETA-Agent] autonomous empirical-ETA sync scheduled.") except Exception as e: logger.warning(f"[ETA-Agent] Sync agent start failed (non-fatal): {e}") # Start the autonomous road-sequencing decision agent. It measures road vs # aerial sequencing on real batches and turns routing_use_road_distance on/off # by itself — no manual flag flip needed. try: from app.services.routing.road_sequencing_agent import get_road_agent from app.config.dynamic_config import get_config get_road_agent().ensure_background_agent( interval_hours=int(get_config().get("routing_eval_interval_hours", 24)), ) logger.info("[RoadAgent] autonomous road-sequencing decision agent scheduled.") except Exception as e: logger.warning(f"[RoadAgent] Decision agent start failed (non-fatal): {e}") logger.info("[OK] Application ready.") yield logger.info("[STOP] Route Optimization API shutting down.") # --------------------------------------------------------------------------- # App # --------------------------------------------------------------------------- app = FastAPI( title="Route Optimization API", version="2.0.0", docs_url="/docs", redoc_url="/redoc", openapi_url="/api/v1/openapi.json", lifespan=lifespan, ) app.add_middleware(RequestIDMiddleware) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], expose_headers=["X-Request-ID", "X-Process-Time"], ) app.add_middleware(GZipMiddleware, minimum_size=1000) @app.middleware("http") async def add_process_time_header(request: Request, call_next): start = time.time() response = await call_next(request) response.headers["X-Process-Time"] = str(round(time.time() - start, 4)) response.headers["X-API-Version"] = "2.0.0" return response app.add_exception_handler(APIException, api_exception_handler) app.add_exception_handler(StarletteHTTPException, http_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(Exception, general_exception_handler) app.include_router(optimization_router) app.include_router(health_router) app.include_router(cache_router) app.include_router(ml_router) app.include_router(ml_web_router) app.include_router(batch_analytics_router) app.include_router(riders_router) @app.get("/", tags=["Root"]) async def root(request: Request): return { "service": "Route Optimization API", "version": "2.0.0", "status": "operational", "docs": "/docs", } if __name__ == "__main__": import uvicorn uvicorn.run("app.main:app", host="0.0.0.0", port=8002, reload=True)