feat: Mount LoRRI integration router in main app + always init DB + open CORS for production
#11
by MouleeswaranM - opened
- brain/app/main.py +57 -42
brain/app/main.py
CHANGED
|
@@ -47,7 +47,7 @@ async def lifespan(app: FastAPI):
|
|
| 47 |
# Startup
|
| 48 |
logger.info(f"Starting {settings.app_title} v{settings.app_version} (env={settings.app_env})")
|
| 49 |
|
| 50 |
-
# Always initialize
|
| 51 |
try:
|
| 52 |
from app.database import init_db, check_db_health
|
| 53 |
await init_db()
|
|
@@ -57,7 +57,7 @@ async def lifespan(app: FastAPI):
|
|
| 57 |
else:
|
| 58 |
logger.warning("Database init succeeded but health check failed")
|
| 59 |
except Exception as e:
|
| 60 |
-
logger.warning(f"Database initialization failed - running
|
| 61 |
|
| 62 |
yield
|
| 63 |
# Shutdown
|
|
@@ -69,26 +69,24 @@ app = FastAPI(
|
|
| 69 |
title=settings.app_title,
|
| 70 |
version=settings.app_version,
|
| 71 |
description="""
|
| 72 |
-
## Fair Dispatch System
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
- `POST /
|
| 87 |
-
- `POST /
|
| 88 |
-
- `GET /
|
| 89 |
-
-
|
| 90 |
-
- `POST /api/v1/feedback` - Submit driver feedback
|
| 91 |
-
- `GET /api/v1/agent-events/stream` - SSE stream for agent events
|
| 92 |
""",
|
| 93 |
lifespan=lifespan,
|
| 94 |
docs_url="/docs",
|
|
@@ -96,7 +94,7 @@ app = FastAPI(
|
|
| 96 |
)
|
| 97 |
|
| 98 |
|
| 99 |
-
# Global exception handler
|
| 100 |
@app.exception_handler(Exception)
|
| 101 |
async def global_exception_handler(request: Request, exc: Exception):
|
| 102 |
logger.error(f"Unhandled error on {request.method} {request.url.path}: {exc}", exc_info=True)
|
|
@@ -106,52 +104,70 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
| 106 |
)
|
| 107 |
|
| 108 |
|
| 109 |
-
#
|
| 110 |
app.add_middleware(
|
| 111 |
CORSMiddleware,
|
| 112 |
-
allow_origins=["*"],
|
| 113 |
allow_credentials=True,
|
| 114 |
allow_methods=["*"],
|
| 115 |
allow_headers=["*"],
|
| 116 |
)
|
| 117 |
|
| 118 |
-
#
|
|
|
|
|
|
|
| 119 |
app.include_router(allocation_router, prefix=settings.api_prefix)
|
| 120 |
app.include_router(allocation_langgraph_router, prefix=settings.api_prefix)
|
|
|
|
|
|
|
| 121 |
app.include_router(drivers_router, prefix=settings.api_prefix)
|
| 122 |
app.include_router(routes_router, prefix=settings.api_prefix)
|
| 123 |
app.include_router(feedback_router, prefix=settings.api_prefix)
|
| 124 |
app.include_router(driver_api_router, prefix=settings.api_prefix)
|
|
|
|
|
|
|
| 125 |
app.include_router(admin_router, prefix=settings.api_prefix)
|
| 126 |
app.include_router(admin_learning_router, prefix=settings.api_prefix)
|
|
|
|
|
|
|
| 127 |
app.include_router(consolidation_router, prefix=settings.api_prefix)
|
| 128 |
|
| 129 |
-
#
|
| 130 |
app.include_router(agent_events_router)
|
| 131 |
|
| 132 |
-
#
|
| 133 |
app.include_router(runs_router, prefix=settings.api_prefix)
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
@app.get("/", tags=["Health"])
|
| 137 |
async def root():
|
| 138 |
-
"""Root endpoint
|
| 139 |
return {
|
| 140 |
"status": "healthy",
|
| 141 |
"service": settings.app_title,
|
| 142 |
"version": settings.app_version,
|
| 143 |
"docs": "/docs",
|
|
|
|
| 144 |
}
|
| 145 |
|
| 146 |
|
| 147 |
@app.get("/health", tags=["Health"])
|
| 148 |
async def health_check():
|
| 149 |
-
"""Health check
|
| 150 |
from app.database import check_db_health
|
| 151 |
db_ok = await check_db_health()
|
| 152 |
-
status_str = "healthy" if db_ok else "degraded"
|
| 153 |
return {
|
| 154 |
-
"status":
|
| 155 |
"database": "connected" if db_ok else "disconnected",
|
| 156 |
"version": settings.app_version,
|
| 157 |
}
|
|
@@ -159,7 +175,7 @@ async def health_check():
|
|
| 159 |
|
| 160 |
@app.get("/api/v1/health", tags=["Health"])
|
| 161 |
async def api_health():
|
| 162 |
-
"""API health
|
| 163 |
from app.database import check_db_health
|
| 164 |
db_ok = await check_db_health()
|
| 165 |
return {
|
|
@@ -168,10 +184,12 @@ async def api_health():
|
|
| 168 |
"version": settings.app_version,
|
| 169 |
"agents": ["ml_effort", "route_planner", "fairness_manager", "driver_liaison", "final_resolution", "explainability"],
|
| 170 |
"langgraph": True,
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
|
| 174 |
-
#
|
|
|
|
| 175 |
if FRONTEND_DIR.exists():
|
| 176 |
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
|
| 177 |
|
|
@@ -179,18 +197,15 @@ if FRONTEND_DIR.exists():
|
|
| 179 |
|
| 180 |
@app.get("/demo/allocate", tags=["Demo"])
|
| 181 |
async def demo_allocate():
|
| 182 |
-
"""Serve the API demo page
|
| 183 |
-
|
| 184 |
-
return FileResponse(demo_path, media_type="text/html", headers=NO_CACHE)
|
| 185 |
|
| 186 |
@app.get("/demo/visualization", tags=["Demo"])
|
| 187 |
async def demo_visualization():
|
| 188 |
"""Serve the agent visualization page."""
|
| 189 |
-
|
| 190 |
-
return FileResponse(viz_path, media_type="text/html", headers=NO_CACHE)
|
| 191 |
|
| 192 |
@app.get("/demo/consolidation", tags=["Demo"])
|
| 193 |
async def demo_consolidation():
|
| 194 |
-
"""Serve the
|
| 195 |
-
|
| 196 |
-
return FileResponse(path, media_type="text/html", headers=NO_CACHE)
|
|
|
|
| 47 |
# Startup
|
| 48 |
logger.info(f"Starting {settings.app_title} v{settings.app_version} (env={settings.app_env})")
|
| 49 |
|
| 50 |
+
# Always initialize DB (creates tables for SQLite fallback)
|
| 51 |
try:
|
| 52 |
from app.database import init_db, check_db_health
|
| 53 |
await init_db()
|
|
|
|
| 57 |
else:
|
| 58 |
logger.warning("Database init succeeded but health check failed")
|
| 59 |
except Exception as e:
|
| 60 |
+
logger.warning(f"Database initialization failed - running degraded: {e}")
|
| 61 |
|
| 62 |
yield
|
| 63 |
# Shutdown
|
|
|
|
| 69 |
title=settings.app_title,
|
| 70 |
version=settings.app_version,
|
| 71 |
description="""
|
| 72 |
+
## FairRelay β AI-Powered Fair Dispatch System
|
| 73 |
+
|
| 74 |
+
Production API for fairness-focused route allocation in logistics.
|
| 75 |
+
Integrates with LoRRI TMS (logisticsnow.in) as an AI intelligence layer.
|
| 76 |
+
|
| 77 |
+
### Architecture: 6-Agent LangGraph Pipeline
|
| 78 |
+
1. **ML Effort Agent** β Computes driver-route effort matrix
|
| 79 |
+
2. **Route Planner** β OR-Tools optimal assignment
|
| 80 |
+
3. **Fairness Manager** β Gini index evaluation, may trigger re-optimization
|
| 81 |
+
4. **Driver Liaison** β Per-driver negotiation (accept/counter)
|
| 82 |
+
5. **Final Resolution** β Resolves counter-proposals via swaps
|
| 83 |
+
6. **Explainability** β Human-readable allocation explanations
|
| 84 |
+
|
| 85 |
+
### LoRRI Integration
|
| 86 |
+
- `POST /lorri/allocate` β Production endpoint with API key auth
|
| 87 |
+
- `POST /lorri/wellness` β Driver wellness scoring
|
| 88 |
+
- `GET /lorri/health` β Integration health monitoring
|
| 89 |
+
- Webhook callbacks on allocation completion
|
|
|
|
|
|
|
| 90 |
""",
|
| 91 |
lifespan=lifespan,
|
| 92 |
docs_url="/docs",
|
|
|
|
| 94 |
)
|
| 95 |
|
| 96 |
|
| 97 |
+
# Global exception handler
|
| 98 |
@app.exception_handler(Exception)
|
| 99 |
async def global_exception_handler(request: Request, exc: Exception):
|
| 100 |
logger.error(f"Unhandled error on {request.method} {request.url.path}: {exc}", exc_info=True)
|
|
|
|
| 104 |
)
|
| 105 |
|
| 106 |
|
| 107 |
+
# CORS β allow all for demo/hackathon, restrict in production
|
| 108 |
app.add_middleware(
|
| 109 |
CORSMiddleware,
|
| 110 |
+
allow_origins=["*"],
|
| 111 |
allow_credentials=True,
|
| 112 |
allow_methods=["*"],
|
| 113 |
allow_headers=["*"],
|
| 114 |
)
|
| 115 |
|
| 116 |
+
# βββ API Routers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 117 |
+
|
| 118 |
+
# Core allocation
|
| 119 |
app.include_router(allocation_router, prefix=settings.api_prefix)
|
| 120 |
app.include_router(allocation_langgraph_router, prefix=settings.api_prefix)
|
| 121 |
+
|
| 122 |
+
# Resources
|
| 123 |
app.include_router(drivers_router, prefix=settings.api_prefix)
|
| 124 |
app.include_router(routes_router, prefix=settings.api_prefix)
|
| 125 |
app.include_router(feedback_router, prefix=settings.api_prefix)
|
| 126 |
app.include_router(driver_api_router, prefix=settings.api_prefix)
|
| 127 |
+
|
| 128 |
+
# Admin
|
| 129 |
app.include_router(admin_router, prefix=settings.api_prefix)
|
| 130 |
app.include_router(admin_learning_router, prefix=settings.api_prefix)
|
| 131 |
+
|
| 132 |
+
# Consolidation (5-agent pipeline)
|
| 133 |
app.include_router(consolidation_router, prefix=settings.api_prefix)
|
| 134 |
|
| 135 |
+
# SSE agent events
|
| 136 |
app.include_router(agent_events_router)
|
| 137 |
|
| 138 |
+
# Run-scoped endpoints
|
| 139 |
app.include_router(runs_router, prefix=settings.api_prefix)
|
| 140 |
|
| 141 |
+
# βββ LoRRI Integration (Option C) βββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββ
|
| 142 |
+
try:
|
| 143 |
+
from app.integrations.lorri import router as lorri_router
|
| 144 |
+
app.include_router(lorri_router, prefix="/lorri", tags=["LoRRI Integration"])
|
| 145 |
+
logger.info("β LoRRI integration router mounted at /lorri")
|
| 146 |
+
except ImportError as e:
|
| 147 |
+
logger.warning(f"LoRRI integration not available: {e}")
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# βββ Health & Status ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 151 |
|
| 152 |
@app.get("/", tags=["Health"])
|
| 153 |
async def root():
|
| 154 |
+
"""Root endpoint."""
|
| 155 |
return {
|
| 156 |
"status": "healthy",
|
| 157 |
"service": settings.app_title,
|
| 158 |
"version": settings.app_version,
|
| 159 |
"docs": "/docs",
|
| 160 |
+
"lorri_integration": "/lorri/health",
|
| 161 |
}
|
| 162 |
|
| 163 |
|
| 164 |
@app.get("/health", tags=["Health"])
|
| 165 |
async def health_check():
|
| 166 |
+
"""Health check with DB verification."""
|
| 167 |
from app.database import check_db_health
|
| 168 |
db_ok = await check_db_health()
|
|
|
|
| 169 |
return {
|
| 170 |
+
"status": "healthy" if db_ok else "degraded",
|
| 171 |
"database": "connected" if db_ok else "disconnected",
|
| 172 |
"version": settings.app_version,
|
| 173 |
}
|
|
|
|
| 175 |
|
| 176 |
@app.get("/api/v1/health", tags=["Health"])
|
| 177 |
async def api_health():
|
| 178 |
+
"""API health for frontend connectivity."""
|
| 179 |
from app.database import check_db_health
|
| 180 |
db_ok = await check_db_health()
|
| 181 |
return {
|
|
|
|
| 184 |
"version": settings.app_version,
|
| 185 |
"agents": ["ml_effort", "route_planner", "fairness_manager", "driver_liaison", "final_resolution", "explainability"],
|
| 186 |
"langgraph": True,
|
| 187 |
+
"lorri_integration": True,
|
| 188 |
}
|
| 189 |
|
| 190 |
|
| 191 |
+
# βββ Static Files & Demo Pages ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
+
|
| 193 |
if FRONTEND_DIR.exists():
|
| 194 |
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
|
| 195 |
|
|
|
|
| 197 |
|
| 198 |
@app.get("/demo/allocate", tags=["Demo"])
|
| 199 |
async def demo_allocate():
|
| 200 |
+
"""Serve the API demo page."""
|
| 201 |
+
return FileResponse(FRONTEND_DIR / "demo.html", media_type="text/html", headers=NO_CACHE)
|
|
|
|
| 202 |
|
| 203 |
@app.get("/demo/visualization", tags=["Demo"])
|
| 204 |
async def demo_visualization():
|
| 205 |
"""Serve the agent visualization page."""
|
| 206 |
+
return FileResponse(FRONTEND_DIR / "visualization.html", media_type="text/html", headers=NO_CACHE)
|
|
|
|
| 207 |
|
| 208 |
@app.get("/demo/consolidation", tags=["Demo"])
|
| 209 |
async def demo_consolidation():
|
| 210 |
+
"""Serve the consolidation pipeline visualization."""
|
| 211 |
+
return FileResponse(FRONTEND_DIR / "consolidation.html", media_type="text/html", headers=NO_CACHE)
|
|
|