| """ |
| API endpoints for AI Load Consolidation. |
| |
| POST /consolidate β Run consolidation (LangGraph multi-agent pipeline) |
| POST /consolidate/sync β Run consolidation (synchronous fallback) |
| POST /consolidate/simulate β Compare multiple scenarios |
| """ |
|
|
| from typing import List |
| from uuid import uuid4 |
|
|
| from fastapi import APIRouter, HTTPException |
|
|
| from app.schemas.consolidation import ( |
| ConsolidationRequest, |
| SimulationRequest, |
| ConsolidationResult, |
| ) |
| from app.services.consolidation_engine import run_consolidation_pipeline |
|
|
| router = APIRouter(prefix="/consolidate", tags=["Consolidation"]) |
|
|
|
|
| @router.post("", response_model=ConsolidationResult) |
| async def consolidate(req: ConsolidationRequest): |
| """ |
| Run AI Load Consolidation through the 5-agent LangGraph pipeline. |
| |
| Agents executed in order: |
| 1. GeoClusteringAgent β geographic proximity clustering |
| 2. TimeWindowAgent β time-window compatibility filtering |
| 3. CapacityOptimizationAgent β FFD bin-packing |
| 4. ScoringConfidenceAgent β AI confidence + optimization score |
| 5. ContinuousLearningAgent β actionable insights |
| """ |
| if not req.shipments: |
| raise HTTPException(400, "shipments array must not be empty") |
| if not req.trucks: |
| raise HTTPException(400, "trucks array must not be empty") |
|
|
| shipments = [s.model_dump() for s in req.shipments] |
| trucks = [t.model_dump() for t in req.trucks] |
| options = req.options.model_dump() |
|
|
| try: |
| from app.services.consolidation_workflow import invoke_consolidation_workflow |
| result = await invoke_consolidation_workflow( |
| shipments=shipments, |
| trucks=trucks, |
| options=options, |
| thread_id=str(uuid4()), |
| ) |
| except Exception as e: |
| |
| print(f"LangGraph consolidation failed ({e}), using sync pipeline") |
| result = run_consolidation_pipeline(shipments, trucks, options) |
|
|
| return ConsolidationResult(**result) |
|
|
|
|
| @router.post("/sync", response_model=ConsolidationResult) |
| async def consolidate_sync(req: ConsolidationRequest): |
| """ |
| Run consolidation using the synchronous multi-agent pipeline (no LangGraph). |
| Useful for environments without LangGraph installed. |
| """ |
| if not req.shipments: |
| raise HTTPException(400, "shipments array must not be empty") |
| if not req.trucks: |
| raise HTTPException(400, "trucks array must not be empty") |
|
|
| shipments = [s.model_dump() for s in req.shipments] |
| trucks = [t.model_dump() for t in req.trucks] |
| options = req.options.model_dump() |
|
|
| result = run_consolidation_pipeline(shipments, trucks, options) |
| return ConsolidationResult(**result) |
|
|
|
|
| @router.post("/simulate") |
| async def simulate_scenarios(req: SimulationRequest): |
| """ |
| Compare multiple consolidation strategies side-by-side. |
| Returns results for each scenario and a recommendation. |
| """ |
| if not req.shipments or not req.trucks: |
| raise HTTPException(400, "shipments and trucks arrays required") |
| if not req.scenarios: |
| raise HTTPException(400, "at least one scenario required") |
|
|
| shipments = [s.model_dump() for s in req.shipments] |
| trucks = [t.model_dump() for t in req.trucks] |
|
|
| results = [] |
| for sc in req.scenarios: |
| opts = { |
| "maxGroupRadiusKm": sc.maxGroupRadiusKm, |
| "timeWindowToleranceMinutes": sc.timeWindowToleranceMinutes, |
| "scenarioName": sc.name, |
| } |
| r = run_consolidation_pipeline(shipments, trucks, opts) |
| results.append({"name": sc.name, **r}) |
|
|
| best = max(results, key=lambda r: r["metrics"]["optimizationScore"]) |
|
|
| return { |
| "scenarios": results, |
| "recommendation": best["name"], |
| } |
|
|