An accessible grievance chatbot. Residents report accessibility barriers, civic problems and disaster/climate impacts to a Telegram bot in Telugu or English, and every report is structured, triaged, grouped with repeat reports of the same problem, and made available through a REST API for downstream systems.
Reporting a broken ramp, an open manhole or a flooded street usually means finding the right office, filling in a form in English and describing the location precisely. For people with disabilities, caregivers and residents who are more comfortable in Telugu, that is often where the report ends.
This project removes that barrier by meeting residents in a messaging app they already use. The bot asks one simple question at a time, accepts taps or typed numbers instead of requiring precise touch input, lets residents type a landmark instead of dragging a map pin, and reads the whole report back for confirmation before submitting it.
- A resident messages the bot on Telegram.
telegram.pyreceives the update (long polling in development, a webhook when deployed) anddispatch.pyhands it on, ignoring duplicate deliveries and handling one message per resident at a time.flow.pywalks the resident through the report: language, category, location, sub-category, description, photo, who is affected, then a read-back to confirm or edit.- On confirmation the report is saved to SQLite, with any photos stored in the database.
classify.pytriages it with keyword rules (barrier type, department, severity) andcluster.pygroups it with nearby open reports of the same problem.- Downstream systems and dashboards read reports, clusters and statistics from the REST API.
The conversation flow is channel-neutral (app/messenger.py); Telegram is the only adapter today.
| Path | Type | Description |
|---|---|---|
app/main.py |
File | FastAPI app: the read API and the Telegram webhook endpoint |
app/telegram_cli.py |
File | Bot commands: poll, set-webhook, delete-webhook, whoami, set-profile |
app/telegram.py |
File | Telegram Bot API adapter: parse updates, send messages, download photos |
app/messenger.py |
File | Channel-neutral Incoming message type and Messenger interface |
app/dispatch.py |
File | Duplicate filtering, per-resident locking, hourly cleanup of abandoned photo folders |
app/flow.py |
File | Conversation state machine, from language choice to submitted report |
app/places.py |
File | Typed place-name search (OpenStreetMap Nominatim), limited to the city |
app/pipeline.py |
File | Post-submission processing: classify, then cluster |
app/classify.py |
File | Keyword-rules triage: barrier type, department, severity |
app/cluster.py |
File | Groups repeat reports of the same physical problem |
app/geo.py |
File | Ward lookup and distances |
app/taxonomy.py |
File | Loads categories, sub-categories and departments from data/ |
app/i18n.py |
File | Language registry |
app/messages_te.py |
File | Every Telugu string the bot sends, with English glosses, for review with OPDs |
app/messages_en.py |
File | English strings (same names as the Telugu file) |
app/audit.py |
File | Stage-wise interaction log and the HMAC reporter_hash |
app/auth.py |
File | The WRITE_ACCESS switch for endpoints that change data |
app/models.py |
File | SQLAlchemy tables |
app/schemas.py |
File | API request/response models, the contract downstream consumers build against |
app/db.py |
File | SQLite engine and table creation |
app/config.py |
File | Settings and .env loader |
app/fake_inbound.py |
File | Builds fake Telegram updates for the tests |
app/api/ |
Directory | REST endpoints: reports, clusters, stats, reference data |
data/taxonomy.json |
File | Categories and sub-categories (Telugu labels, default department, municipal subject) |
data/departments.json |
File | Placeholder department list |
data/wards.geojson |
File | Optional ward polygons; empty for now, so reports have no ward |
docs/schema.md |
File | Database schema reference |
tests/ |
Directory | API, conversation flow, dashboard and unit tests |
pyproject.toml |
File | Project metadata and Python dependencies (managed with uv) |
uv.lock |
File | Locked dependency versions, generated by uv |
.env |
File | Local secrets (gitignored, copied from .env.example) |
.env.example |
File | Variable names with placeholder values |
- uv: no separate Python install needed;
uvdownloads Python 3.12 or newer automatically if it isn't on your machine.
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh- A Telegram bot token: in Telegram, message @BotFather, send
/newbotand copy the token it gives you.
git clone https://github.com/CivicDataLab/PI4C
cd PI4Cuv syncThis creates .venv/, installs Python if needed, and installs the locked dependencies from uv.lock. Check that everything works with:
uv run pytest# Windows
copy .env.example .env
# macOS / Linux
cp .env.example .envThen edit .env. It is gitignored; keep real tokens and keys out of .env.example, the README and source files.
| Variable | Required | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
Yes | Bot token from @BotFather |
WRITE_ACCESS |
No | true lets anyone change reports and see conversation logs; false (the default) blocks that for everyone. Reading is always open |
TELEGRAM_WEBHOOK_SECRET |
When deployed | Random string Telegram sends with every webhook call. Without it, anyone who finds the webhook URL can submit fake reports |
DATABASE_URL |
No | Another SQLite file, as sqlite:///.... Defaults to data/pi4c.db |
Telegram bot (local development, no public URL needed):
uv run python -m app.telegram_cli pollOpen your bot in Telegram and send /start.
Read API (runs separately):
uv run uvicorn app.main:app --reloadInteractive docs are at http://localhost:8000/docs. Import http://localhost:8000/openapi.json into Postman to try the endpoints.
Webhook mode (deployed server): set TELEGRAM_WEBHOOK_SECRET, run uvicorn behind HTTPS, then register the webhook:
uv run python -m app.telegram_cli set-webhook https://<host>/telegram/webhookPolling and a webhook can't run at the same time; poll deletes the webhook when it starts.
| Step | Resident sees | Accepts |
|---|---|---|
| LANGUAGE | తెలుగు / English (first contact and every /start) |
tap |
| MENU | 3 buttons: Accessibility / General Civic / Disaster-Climate | tap, or type 1–3 |
| LOCATION | Live location, Choose a location and Type the place name keyboard buttons | device location, live location, a place picked via the attach (paperclip) menu → Location, or a typed place name |
| LOCATION_PICK | up to 5 matching places as a numbered list, plus "None of these" | tap, type the number, or type another name to search again |
| SUBCATEGORY | numbered list + buttons | tap, or type the number |
| DESCRIBE | free text prompt | typed text, or a photo with a caption (voice notes are refused) |
| PHOTO | optional, "No photo" button | photo or skip |
| AFFECTED | self (PwD) / caregiver / general | tap |
| READBACK | summary + Confirm / Edit / Cancel | Edit jumps to one step, then back here |
/start, "మెనూ" or "hi" restarts; /cancel or "రద్దు" cancels. Sessions expire after 30 minutes of silence. Only private chats are handled; group messages are ignored.
pwd_marker is true when the reporter is a person with disability or a caregiver, or the category is accessibility.
Bot profile: the description, "About" text and /start /language /cancel menu live in BOT_DESCRIPTION, BOT_SHORT_DESCRIPTION and BOT_COMMANDS in app/messages_en.py and app/messages_te.py. After editing them, publish with uv run python -m app.telegram_cli set-profile. English is the default; Telugu is shown to users whose Telegram app is set to Telugu.
All endpoints are under /api/v1 and need no key. Read endpoints are open to anyone. Write endpoints (changing reports, the conversation log) follow WRITE_ACCESS: open to anyone when it's true, closed to everyone when it's false.
| Endpoint | Access | Description |
|---|---|---|
GET /reports |
public | Filters: category, subcategory, barrier_type, ward, department_id, severity, status, pwd_marker, needs_review, cluster_id, created_from/to, updated_since, bbox; paginated |
GET /reports/{id} |
public | Full record, including media links |
PATCH /reports/{id} |
write access | status, status_note, department_id, severity, municipal_subject, needs_review |
GET /reports/{id}/media/{media_id} |
public | Photo (from the database) |
GET /reports/{id}/log |
write access | Stage-wise interaction log |
GET /reports.geojson |
public | Same filters, as GeoJSON points |
GET /reports.csv |
public | Same filters, every matching report as CSV (Excel / Power BI); UTF-8 with BOM |
GET /clusters, /clusters/{id} |
public | Distinct problems with report counts |
GET /stats/summary |
public | Headline numbers: total, open, resolved, PwD, needs review, high/critical, with photo |
GET /stats/counts?group_by=… |
public | Counts grouped by any of category, status, severity, ward, department_id, pwd_marker… (repeat group_by) |
GET /stats/timeseries?interval=day|week|month |
public | Reports per period (UTC), optional group_by split |
GET /stats/by-ward, /by-category, /by-department |
public | Grouped counts |
GET /metrics |
public | Completion rate, read-back confirm/edit, review load, classifier mix |
GET /taxonomy, /departments, /wards |
public | Reference data |
All /stats/* endpoints and /reports.csv take the same filters as /reports.
External dashboards. Server-side tools (Power BI web connector, Grafana Infinity, Metabase, scripts) can call the read endpoints directly, with no key. Web pages on any site can call it from the browser too: CORS allows every origin.
data/departments.json: placeholder department list.handlesdescribes what each department deals with.data/taxonomy.json: categories and sub-categories (Telugu labels, default department, municipal subject).data/wards.geojson: optional ward polygons (properties.wardorproperties.name); reports getwardset from their location. Empty for now.app/messages_te.py: every Telugu string the bot sends, with English glosses, for review with OPDs and native speakers.
Run the tests:
uv run pytestCheck which bot the token belongs to:
uv run python -m app.telegram_cli whoami