Initial commit
This commit is contained in:
192
app/main.py
Normal file
192
app/main.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""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}")
|
||||
|
||||
# Warm up the Thompson Sampling bandit (bootstraps from historical DB)
|
||||
try:
|
||||
from app.services.ml.strategy_bandit import get_bandit
|
||||
bandit = get_bandit()
|
||||
stats = bandit.get_stats()
|
||||
logger.info(
|
||||
f"[Bandit] RL strategy bandit ready — "
|
||||
f"{stats['context_count']} contexts, "
|
||||
f"{stats['total_updates']} historical updates loaded."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Bandit] Warm-up 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)
|
||||
Reference in New Issue
Block a user