One photo. Seven agents. Under ten seconds.
A citizen reports a civic issue. Specialised AI agents classify, dedup, route, verify, schedule (MILP), audit the fix, and forecast tomorrow's hotspots. Every agent's decision is auditable; an LLM never makes a load-bearing call without a deterministic gate verifying it.
The citizen flow, end-to-end
What happens between “snap a photo or video” and “crew shows up”.
The 7 agents
Reads your photo or short video → identifies what's wrong (pothole? streetlight? sewage?), how bad it is (severity 1-5), and how confident the model is.
Checks if anyone within 50 metres already reported the same thing. If yes, merges into that ticket so the crew isn't sent twice.
The LLM proposes which department to route to. A deterministic SOP table verifies the proposal — every disagreement is logged and the safe default wins.
Notifies the 5 nearest citizens. After 3 confirmations, status is promoted to VERIFIED and the dispatcher picks it up.
THIS is the math. Re-solves the entire crew dispatch problem for tomorrow each time a new issue arrives — picks crews + stop order to minimize severity-weighted delays + km driven.
When the crew uploads an after-photo, two layers audit it: CLIP confirms same location, the pothole CNN confirms it's actually fixed. Catches fake closures.
Feeds the rainfall × ward panel that predicts next-30-day hotspots. Lets dispatchers pre-position crews before complaints flood in.
How MILP actually solves dispatch
The single most important agent — and the easiest to misunderstand.
At any moment Bengaluru has hundreds of open civic issues and a dozen BBMP crews. The naive answer is FIFO — assign each new issue to the first crew with capacity, ignore the map. That's how dispatch works today.
MILP rearranges the day every time a new issue lands. It picks which crew visits which issues in what order, weighing the cost of:
- Severity × lateness — a sev-5 sewage overflow is penalised 5× more than a sev-1 garbage report when running past SLA
- Total km driven — bunches geographically close stops into one route
- Unserved issues — pays a heavy penalty for tickets the day can't fit
Subject to: each crew's skill (Roads ≠ Water), capacity (~10 stops/day), and shift hours. Solver: Google OR-Tools, branch-and-cut + guided local search, 15-second cap.
That's why your Hongasandra report showed scheduled_for: ('7cae7e1c…', 0) — crew 7cae, stop position 0. The MILP literally moved your high-severity issue to the front of the crew's day.
| Load | FIFO km | MILP km | Δ |
|---|---|---|---|
| 120 / 12 crews | 874 | 52 | −94% |
| 250 / 12 crews | 1,509 | 104 | −93% |
| 800 / 12 crews | 1,019 | 107 | −89.5% |
scripts/run_real_backtest.py.Predictive insights — forecasting tomorrow's hotspots
What the 7th agent feeds, and how it closes the loop back to MILP.
Every report logged into a panel of ward × month × rainfall_mm × rainfall_lag1. We trained a HistGradientBoosting regressor on 14,580 real ward-months of Bengaluru data (2021–2025):
log(road_complaints + 1) ~ ward_FE + month_FE + rainfall_mm + rainfall_mm_lag1
In plain words: given a ward, the calendar month, today's rainfall and last month's rainfall, how many pothole complaints will this ward see next month?
The model hits R² = 0.871 on the 2025 hold-out year. The map's hotspot heatmap is its output — wards in red are predicted to spike, so the dispatcher pre-positions crews there instead of waiting for the citizen flood.
Closes the loop: your report nudges its ward's count up → predictor raises the heatmap there → MILP weights that ward heavier in tomorrow's solve — even before more reports come in.
- 14,580 ward-month observations (real)
- 60 months of Bengaluru rainfall (real IMD-style panel)
- 243 KGIS ward polygons
- Every new report appended live by the Insights agent
scripts/build_hotspots.py → writes data/processed/hotspots.geojson consumed by /map.Hub & spoke — how a ticket leaves NagarikAI
We aren't a parallel BBMP. NagarikAI is the citizen front door + AI triage; each department keeps using whatever software they already have. Once Triage picks a department, delivery.py pushes the ticket out via that department's preferred channel. The supervisor dashboard is the fallback for departments that don't have their own system.
- · Citizen submit (photo / video / location)
- · 7-agent triage loop
- · MILP scheduler
- · delivery.py dispatches to spoke
- · sla_watcher.py escalates on silence
Escalation ladder — what happens when the dept goes silent
Runs every 60s in apps/api/nagarik/jobs/sla_watcher.py. Every state change writes a citizen notification — they always know where their ticket stands.
Tech stack at a glance
docs/PITCH.md, REAL_DATA.md, and INBOUND_CHANNELS.md.