#+ RouteSense — AI Logistics Control Center
RouteSense is a full-stack, AI-assisted logistics monitoring + control demo.
It simulates a live logistics network (shipments, warehouses, carriers, routes), continuously emits events into Postgres, and runs an Observe → Detect → Reason → Decide → Act → Learn agent loop that turns those events into operator-facing decisions (reroutes, carrier switches, alerts).
This repo contains:
- Backend (
backend/): FastAPI API + SimPy discrete-event simulator + LangGraph agent + Postgres/PostGIS persistence. - Frontend (
frontend/): Next.js dashboard UI that visualizes the system state and decisions.
The simulation engine (backend/app/simulation.py) runs continuously and updates the database each tick:
- advances shipment status (
created → dispatched → in_transit → delivered, plusdelayed/failed) - fluctuates warehouse load + congestion
- updates route traffic + weather over locally cached maritime route geometries
- occasionally injects disruptions (pickup failures, ETA drift, carrier degradation, congestion spikes)
- records every noteworthy change as a row in
simulation_events
FastAPI routers (backend/app/routers/) expose read and control endpoints:
- list shipments/warehouses/carriers/routes
- retrieve the event stream
- create shipments and trigger scenario endpoints to force disruptions
- return GeoJSON for routes/points (PostGIS-backed) for map rendering
The agent loop (backend/app/ai_agent/agent.py) runs in the background alongside the simulator:
- warm-starts an ML delay-risk model on synthetic data, then retrains it from resolved learning outcomes (
backend/app/ai_agent/risk_models.py) - compiles a LangGraph pipeline (
backend/app/ai_agent/graph.py,backend/app/ai_agent/nodes.py) - on each cycle, scans DB state/events and produces:
- a structured set of detected risks
- a decision recommendation with evidence + confidence
- (optionally) an LLM-generated natural-language summary
- writes every decision to
decision_logso the UI can show history and approvals - snapshots delay-risk model features into decision evidence, resolves outcomes, and periodically retrains the delay model with real learning samples
Two ways actions can happen:
- Agent approval workflow:
POST /api/agent/approve/{decision_id}executes the recommended action when an operator approves. - Direct operator actions:
POST /api/actions/*endpoints let the UI trigger actions directly (for demo/MVP flows).
┌───────────────────────────────────────────────────────────────────────┐
│ Frontend │
│ Next.js dashboard (React) │
│ - consumes REST API + renders maps, tables, decision timelines │
└───────────────────────────────┬───────────────────────────────────────┘
│ HTTP (JSON)
┌───────────────────────────────┴───────────────────────────────────────┐
│ Backend │
│ FastAPI │
│ Routers: /api/data, /api/simulate, /api/geo, /api/agent, /api/actions │
│ │
│ Background services (started on app startup): │
│ 1) SimPy Simulation thread → updates entities + writes events │
│ 2) LangGraph Agent thread → detects risks + logs decisions │
│ 3) Lifecycle manager → keeps population of active shipments │
└───────────────────────────────┬───────────────────────────────────────┘
│ SQLAlchemy
┌───────────────────────────────┴───────────────────────────────────────┐
│ PostgreSQL 16 + PostGIS │
│ Tables: shipments, warehouse_state, carrier_performance, routes, │
│ simulation_events, decision_log │
└───────────────────────────────────────────────────────────────────────┘
- FastAPI (API framework) + Uvicorn (ASGI server)
- SQLAlchemy 2.x (ORM)
- PostgreSQL 16 + PostGIS (spatial types: points, linestrings)
- GeoAlchemy2 + Shapely (geometry conversions)
- SimPy (discrete-event simulation)
- LangGraph (agent graph orchestration)
- google-genai (Gemini client used by the LLM node; optional)
- scikit-learn / pandas / numpy (feature engineering + ML delay-risk model)
- uv (Python packaging + lockfile)
- Next.js 15 (React 19)
- TypeScript
- Tailwind CSS + shadcn/ui (Radix primitives)
- Charts: Recharts
- Maps: react-simple-maps
README.md— this documentdocs/risk-model-feedback-loop.md— how resolved learning outcomes retrain the delay-risk modelpyproject.toml— intentionally minimal (real configs live inbackend/andfrontend/)uv.lock— lockfile for the root (mostly unused; backend has its own)
backend/main.py— FastAPI entrypoint; starts background services; defines/healthbackend/app/config.py— environment variables (DB URL, tick intervals, thresholds, origins)database.py— SQLAlchemy engine/sessionmodels.py— ORM schema (shipments, routes, events, decision log)schemas.py— Pydantic response/request modelsseed.py— seed initial warehouses/carriers/routes/shipmentssimulation.py— SimPy simulation engine producing live eventslifecycle.py— keeps system “alive” (maintains active shipment set)routers/data.py— read APIs (shipments/warehouses/carriers/routes/events)simulate.py— simulation control + scenario triggersgeo.py— geo endpoints returning GeoJSON (for map UI)agent.py— agent endpoints (risks, decisions, approvals, summaries)actions.py— direct operator action endpoints (alert/reroute/switch carrier)
ai_agent/— core decision engineagent.py— backgroundAgentLoopgraph.py/nodes.py— LangGraph pipeline definitionrisk_models.py— ML delay risk model with synthetic warm-start + hybrid retrainingactions.py— action implementations used by agent + operator endpointsllm_client.py— Gemini client + fallback behaviorslearning.py— metrics/learning loop fed by outcomes, including delay-model retraining samples
data/maritime_routes.json— offline port coordinates and maritime waypoints used to seed ship routes
backend/docker-compose.yaml— local dev stack (Postgres + API)backend/Dockerfile— container build used for Railwaybackend/railway.toml— Railway service config (healthcheck etc.)backend/schema.sql— DDL snapshot (useful for inspection/bootstrapping)backend/tests/— pytest suite
frontend/app/— route tree (Next App Router pages)frontend/components/— UI building blocks (tables, charts, maps, reasoning)frontend/lib/api.ts— API client +NEXT_PUBLIC_API_URLwiringfrontend/package.json— frontend deps + scripts
backend/main.py uses FastAPI lifespan to start heavy work in a background task:
- wait for Postgres (
SELECT 1retry loop) - ensure PostGIS extension exists (
CREATE EXTENSION IF NOT EXISTS postgis) Base.metadata.create_all(...)to create/verify tables- seed initial data (idempotent)
- start simulation thread
- start agent loop thread
- start lifecycle manager
GET /health always returns HTTP 200 with { ready: boolean } so container health checks don’t fail during the longer initialization.
- Docker + Docker Compose
- For host-based backend dev: Python 3.11+ +
uv - For frontend dev: Node.js (npm)
From the backend/ folder:
docker compose up --buildAPI:
- http://localhost:8000
- Swagger docs: http://localhost:8000/docs
cd backend
docker compose up postgres -d
cp .env.example .env
uv sync
uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000cd frontend
npm install
npm run devSet the API base URL:
- Local dev: defaults to
http://localhost:8000 - For deployments: set
NEXT_PUBLIC_API_URL=https://<your-backend-domain>
Backend env vars live in backend/.env.example and are read by backend/app/config.py.
Common ones:
DATABASE_URL— Postgres connection stringSIM_TICK_INTERVAL— real seconds between ticks (default2.0)SIM_SPEED— sim minutes advanced per real second (default10.0)SEED_WAREHOUSES,SEED_CARRIERS,SEED_ROUTES,SEED_SHIPMENTSAGENT_TICK_INTERVAL— seconds between agent cycles- thresholds such as
RISK_THRESHOLD,BOTTLENECK_THRESHOLD,CARRIER_RELIABILITY_THRESHOLD - delay-model retraining knobs such as
RISK_MODEL_RETRAIN_INTERVAL_CYCLES,RISK_MODEL_RETRAIN_MIN_SAMPLES,RISK_MODEL_RETRAIN_SYNTHETIC_SAMPLES,RISK_MODEL_REAL_SAMPLE_WEIGHT - LLM settings such as
GEMINI_API_KEY,LLM_CALL_COOLDOWN - CORS allowlist:
ALLOWED_ORIGINS
The delay-risk model starts with synthetic data so the agent can score shipments immediately after startup. As the agent acts, delay-risk decisions store their exact model feature snapshot in decision_log.evidence. The Learn step later resolves those decisions into actual labels (success means within SLA, failed means missed/projected-missed SLA or failed) and periodically retrains the active model with a hybrid synthetic + real dataset.
See docs/risk-model-feedback-loop.md for the full flow, configuration, and current limitations.
All backend endpoints are served under the FastAPI app in backend/main.py.
GET /— service info (includesstatus: starting|ready)GET /health— liveness + readiness flag
GET /api/shipmentsGET /api/warehousesGET /api/carriersGET /api/routesGET /api/events
POST /api/simulate/startPOST /api/simulate/stopGET /api/simulate/status- scenario triggers like
warehouse-congestion,traffic-spike,pickup-failure,eta-drift, etc.
GET /api/agent/statusGET /api/agent/risksGET /api/agent/decisionsPOST /api/agent/analyze(manual cycle)POST /api/agent/approve/{decision_id}(approval workflow)GET /api/agent/summary
POST /api/actions/send-alertPOST /api/actions/reroutePOST /api/actions/switch-carrier(if enabled inactions.py)
These are the main tables.
shipments— shipments with status, ETA/SLA, and optional geometry pointswarehouse_state— capacity/load/queue/congestion and optional geometrycarrier_performance— reliability, delay probability, pickup rate, totalsroutes— origin/destination, distance, traffic/weather, optional line geometrysimulation_events— immutable event stream emitted by simulatordecision_log— agent decision history + approval / outcome tracking
For full DDL, see backend/schema.sql.
This repo has been used with:
- Railway for backend + Postgres
backend/Dockerfilerunsuvicorn main:appon port8000backend/railway.tomlsets healthcheck path/health
- Vercel for frontend
- remember
NEXT_PUBLIC_API_URLmust includehttps://
- remember
Backend tests live in backend/tests/.
cd backend
uv run pytest| carrier | VARCHAR(64) | Assigned carrier ID |
| route_id | VARCHAR(64) | Route ID |
| eta | TIMESTAMP | Estimated time of arrival |
| sla_deadline | TIMESTAMP | SLA deadline |
| status | ENUM | created · dispatched · in_transit · at_warehouse · out_for_delivery · delivered · delayed · failed |
| Column | Type | Description |
|---|---|---|
warehouse_id |
VARCHAR(64) | Unique identifier |
location |
VARCHAR(128) | City |
capacity |
INT | Maximum load |
current_load |
INT | Current load |
queue_length |
INT | Queued shipments |
congestion_score |
FLOAT | Computed metric (0 – 1) |
| Column | Type | Description |
|---|---|---|
carrier_id |
VARCHAR(64) | Unique identifier |
name |
VARCHAR(128) | Carrier name |
reliability_score |
FLOAT | Historical reliability (0 – 1) |
delay_probability |
FLOAT | Delay likelihood |
pickup_success_rate |
FLOAT | Pickup success rate |
total_shipments |
INT | Lifetime shipments |
total_delays |
INT | Lifetime delays |
| Column | Type | Description |
|---|---|---|
route_id |
VARCHAR(64) | Unique identifier |
origin |
VARCHAR(128) | Start city |
destination |
VARCHAR(128) | End city |
distance |
FLOAT | Distance in km |
traffic_level |
ENUM | low · moderate · high · severe |
weather_factor |
FLOAT | Weather multiplier (1.0 = clear) |
| Column | Type | Description |
|---|---|---|
event_type |
ENUM | Event category |
entity_id |
VARCHAR(64) | Related entity ID |
payload |
TEXT (JSON) | Event detail payload |
sim_time |
FLOAT | Simulation clock value |
created_at |
TIMESTAMP | Wall-clock timestamp |
| Event | Trigger |
|---|---|
shipment_created |
New shipment added |
shipment_dispatched |
Shipment picked up by carrier |
shipment_delivered |
Shipment reaches destination |
shipment_delayed |
Shipment status moves to delayed |
warehouse_load_update |
Warehouse load changes |
warehouse_congestion |
Congestion score exceeds 0.85 |
carrier_delay_event |
Carrier reliability degrades |
carrier_failure |
Manual carrier failure scenario |
route_traffic_update |
Traffic or weather changes on a route |
pickup_failure |
Carrier fails to pick up a shipment |
eta_drift |
ETA pushed forward due to disruption |
┌─────────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Data API │ │ Simulate API │ │ Health / Docs │ │
│ │ /api/shipments│ │ /api/simulate│ │ / , /health │ │
│ │ /api/warehouses│ │ /start │ │ /docs (Swagger) │ │
│ │ /api/carriers │ │ /stop │ └───────────────────┘ │
│ │ /api/routes │ │ /status │ │
│ │ /api/events │ │ /warehouse-│ │
│ └──────┬───────┘ │ congestion │ │
│ │ │ /carrier- │ │
│ │ │ failure │ │
│ │ │ /traffic- │ │
│ │ │ spike │ │
│ │ └──────┬───────┘ │
│ ┌──────┴─────────────────┴──────────────┐ │
│ │ SimPy Simulation Engine │ │
│ │ • Shipment lifecycle transitions │ │
│ │ • Warehouse load fluctuations │ │
│ │ • Route traffic / weather updates │ │
│ │ • Random disruptions │ │
│ └──────────────────┬────────────────────┘ │
│ │ │
│ ┌──────────────────┴────────────────────┐ │
│ │ PostgreSQL (SQLAlchemy ORM) │ │
│ │ • shipments • warehouse_state │ │
│ │ • carrier_performance • routes │ │
│ │ • simulation_events │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Layer | Technology |
|---|---|
| API | FastAPI + Uvicorn |
| Simulation | SimPy (discrete-event) |
| ORM | SQLAlchemy 2.0 |
| Database | PostgreSQL 16 |
| Data | Pandas, NumPy |
| Package Manager | uv |
docker compose up -duv syncuv run uvicorn main:app --reload --host 0.0.0.0 --port 8000The API will be live at http://localhost:8000 and the interactive docs at http://localhost:8000/docs.
On startup, the server will:
- Create all database tables
- Seed initial warehouses, carriers, routes, and shipments
- Start the background simulation engine
| Method | Path | Description |
|---|---|---|
| GET | / |
Service info |
| GET | /health |
Health check |
| GET | /docs |
Swagger UI |
| Method | Path | Description |
|---|---|---|
| GET | /api/shipments |
List shipments (filter by status) |
| GET | /api/shipments/{id} |
Get single shipment |
| GET | /api/warehouses |
List warehouses (sorted by congestion) |
| GET | /api/warehouses/{id} |
Get single warehouse |
| GET | /api/carriers |
List carriers (sorted by reliability) |
| GET | /api/carriers/{id} |
Get single carrier |
| GET | /api/routes |
List all routes |
| GET | /api/routes/{id} |
Get single route |
| GET | /api/events |
List events (filter by type, entity) |
| Method | Path | Description |
|---|---|---|
| POST | /api/simulate/start |
Start the simulation engine |
| POST | /api/simulate/stop |
Stop the simulation engine |
| GET | /api/simulate/status |
Engine status + counts |
| POST | /api/simulate/warehouse-congestion |
Trigger warehouse congestion |
| POST | /api/simulate/carrier-failure |
Trigger carrier failure cascade |
| POST | /api/simulate/traffic-spike |
Trigger traffic spike on routes |
| POST | /api/simulate/pickup-failure |
Trigger a pickup failure |
| POST | /api/simulate/eta-drift |
Trigger ETA drift on shipments |
| POST | /api/simulate/create-shipment |
Create a new shipment on the fly |
| Column | Type | Description |
|---|---|---|
| shipment_id | VARCHAR(64) | Unique identifier |
| origin | VARCHAR(128) | Source location |
| destination | VARCHAR(128) | Target location |
| carrier | VARCHAR(64) | Assigned carrier ID |
| route_id | VARCHAR(64) | Route ID |
| eta | TIMESTAMP | Estimated time of arrival |
| sla_deadline | TIMESTAMP | SLA deadline |
| status | ENUM | created, dispatched, in_transit, at_warehouse, out_for_delivery, delivered, delayed, failed |
| Column | Type | Description |
|---|---|---|
| warehouse_id | VARCHAR(64) | Unique identifier |
| location | VARCHAR(128) | City |
| capacity | INT | Maximum load |
| current_load | INT | Current load |
| queue_length | INT | Queued shipments |
| congestion_score | FLOAT | Computed metric (0-1) |
| Column | Type | Description |
|---|---|---|
| carrier_id | VARCHAR(64) | Unique identifier |
| name | VARCHAR(128) | Carrier name |
| reliability_score | FLOAT | Historical reliability (0-1) |
| delay_probability | FLOAT | Delay likelihood |
| pickup_success_rate | FLOAT | Pickup success rate |
| total_shipments | INT | Lifetime shipments |
| total_delays | INT | Lifetime delays |
| Column | Type | Description |
|---|---|---|
| route_id | VARCHAR(64) | Unique identifier |
| origin | VARCHAR(128) | Start city |
| destination | VARCHAR(128) | End city |
| distance | FLOAT | Distance in km |
| traffic_level | ENUM | low, moderate, high, severe |
| weather_factor | FLOAT | Weather multiplier |
| Column | Type | Description |
|---|---|---|
| event_type | ENUM | Event category |
| entity_id | VARCHAR(64) | Related entity |
| payload | TEXT (JSON) | Event details |
| sim_time | FLOAT | Simulation clock |
| created_at | TIMESTAMP | Wall-clock time |
shipment_created/shipment_dispatched/shipment_delivered/shipment_delayedwarehouse_load_update/warehouse_congestioncarrier_delay_event/carrier_failureroute_traffic_updatepickup_failureeta_drift