A Multi-Agent Geospatial + Resale-Price Intelligence System for Singapore HDB Search
This project provides an autonomous DeepAgent pipeline that processes natural-language queries like:
- "Find flats near Bukit Batok MRT"
- "Show me 4-room flats in Toa Payoh under 500k"
- "HDB near MRT within 600m with good value picks"
and turns them into:
- Intent extraction (town, MRT station, flat type, max price, radius)
- MRT resolver (maps MRT → nearest HDB planning area via MCP SQL tool)
- Resale flat retrieval via MCP PostgreSQL tools
- Geospatial enrichment via MCP geospatial-query tools
- Distance formatting and correction
- LLM-powered summaries
All orchestrated end-to-end via DeepAgents + LangGraph.
autonomous-hdb-deepagents/
│
├── pyproject.toml
├── README.md
│
├── src/
│ └── autonomous_hdb_deepagents/
│ │ └── __init__.py
│ │
│ ├── agent/
│ │ ├── __init__.py
│ │ ├── cli.py # CLI entrypoint (uv run -m autonomous_hdb_deepagents.agent.cli)
│ │ ├── deep_agent.py # DeepAgent factory + LangGraph orchestration pipeline
│ │ ├── intent.py # LLM-powered intent extraction
│ │ ├── mrt_resolver.py # MRT → HDB-town resolver (MCP SQL)
│ │ ├── resale.py # HDB resale SQL query node
│ │ ├── mrt.py # Geospatial enrichment node
│ │ ├── summary.py # LLM summary generation node
│ │ ├── state.py # PipelineState (Pydantic)
│ │ ├── tools.py # MCP Toolbox loader + caching
│ │ └── llm.py # Shared OpenRouter LLM instance (ChatOpenAI)
│ │
│ ├── api/
│ │ ├── __init__.py
│ │ ├── api_server.py # FastAPI server providing /health + /query
│ │ └── api_server_launch.py # Standalone launcher with controlled PYTHONPATH
│ │
│ └── ui/
│ ├── __init__.py
│ └── gradio_app.py # Gradio full-screen chat UI with sample questions
│
├── notebook/
│ ├── deepagents-multi-agent.ipynb
│ ├── deepagents-sub-agent.ipynb
│ ├── deepagents-custom-model.ipynb
│ └── data-ingestion/
│ ├── hdb-existing-building.ipynb
│ ├── hdb-property-info.ipynb
│ ├── lta-mrt-exits.ipynb
│ ├── moe-sg-schools.ipynb
│ ├── npark-parks.ipynb
│ └── onemap-geocoding-cache.ipynb
│
├── db/
│ └── init/ # Schema + ingestion SQL
│ ├── 00_schema.sql
│ ├── 01_load_data.sql
│ └── data/ # Preprocessed CSV/GeoJSON from notebooks
│ └── *.csv
│
├── tools.yaml
├── start.sh
├── Dockerfile
└── docker-compose.yml
This project includes a set of fully reproducible data-ingestion notebooks located under:
notebook/data-ingestion/These notebooks document how each official dataset from data.gov.sg, LTA, MOE, NParks, and OneMap was:
- Downloaded (CSV/GeoJSON/API)
- Cleaned
- Transformed and normalized
- Converted into SQL-ready tables
- Inserted into PostgreSQL
These notebooks serve as transparent documentation of all preprocessing logic and allow anyone to rebuild the entire database from scratch.
| Notebook | Purpose |
|---|---|
| hdb-existing-building.ipynb | Extracts and processes HDB existing building geometries and metadata. |
| hdb-property-info.ipynb | Loads HDB property information dataset and formats it for SQL ingestion. |
| lta-mrt-exits.ipynb | Processes official LTA MRT exit GeoJSON and creates table-ready geometries. |
| moe-sg-schools.ipynb | Processes MOE schools master list (locations, categories, addresses). |
| npark-parks.ipynb | Loads NParks parks boundaries/points and normalizes names + coordinates. |
| onemap-geocoding-cache.ipynb | Generates and caches OneMap coordinate lookups to speed up ingestion. |
uv syncuv add --dev --editable .Now you can import:
import autonomous_hdb_deepagentsuv run -m autonomous_hdb_deepagents.agent.cli "Find flats near Bukit Panjang MRT"uv run src/autonomous_hdb_deepagents/agent/cli.py "Find flats near Bukit Panjang MRT"[INTENT] Parsed intent → {'town': None, 'mrt_station': 'Bukit Panjang', 'flat_type': None, 'max_price': None, 'mrt_radius': None}
[MRT-RESOLVE] Resolving MRT station: Bukit Panjang
[MRT-RESOLVE] BP → BUKIT PANJANG
[RESALE] Fetching 4 ROOM in BUKIT PANJANG <= 600000...
[RESALE] Retrieved 30 flats
[MRT] Enriching 30 flats (radius=800)
[MRT] Example: BT PANJANG RING RD → BANKIT LRT STATION (162m)
[SUMMARY] Summarizing 30 flats
=== FINAL RESPONSE ===
### **Flats Near Bukit Panjang MRT: Summary**
...
Extracts:
- town
- mrt_station
- flat_type
- max_price
- mrt_radius
- Calls MCP
get-mrt-towns - Resolves
"BB" → "BUKIT BATOK" - Auto-sets
townif missing
Uses MCP SQL tool list-hdb-flats to fetch:
- block
- street
- price
- coordinates
Defaults:
- flat_type="4 ROOM"
- max_price=600000
- town="TOA PAYOH" (fallback)
Uses MCP:
geospatial-query
Adds:
- nearest mrt
- distance raw
- formatted distance (e.g.,
"367m","1.1km")
Smart unit normalization
- degrees → meters
- km → meters
- meters → meters
LLM produces:
- price range
- closest flats
- best value picks
- meaningful insights
Graph:
intent → mrt_resolve → resale → mrt → summary → END
The API lives under:
src/autonomous_hdb_deepagents/api/Start server:
uv run python src/autonomous_hdb_deepagents/api/api_server_launch.pyHealth check
curl http://localhost:8000/healthQuery endpoint (PowerShell-safe):
Invoke-RestMethod -Uri "http://localhost:8000/query" -Method Post `
-Body '{"query":"Find flats near Bukit Batok MRT"}' `
-ContentType "application/json"
# Sample Outputs
# response
# --------
# ### HDB Flats Near Bukit Batok MRT …The project includes a fully interactive Gradio chat UI for your autonomous HDB DeepAgent. This UI is separate from the FastAPI server and can run independently.
- Full-screen responsive chat layout
- Sample question buttons
- Built-in DeepAgent orchestration
- Works 100% locally — no external server needed
- Runs in its own process
From project root:
uv run python src/autonomous_hdb_deepagents/ui/gradio_app.pyYou will see:
* Running on local URL: http://0.0.0.0:7860
Open this in your browser:
➡️ http://localhost:7860You must have these MCP tools available:
list-hdb-flats
get-mrt-towns
geospatial-query
Loaded dynamically via:
ToolboxClient("http://127.0.0.1:5000")✔ Fully modular agent layers
✔ Clear separation of concerns
✔ State management via Pydantic
✔ LangGraph deterministic pipeline
✔ DeepAgent orchestration wrapper
✔ Supports CLI, server, and notebook workflows
✔ Clean Python package for reuse
This section explains how to run the entire system—database, toolbox, backend API, and Gradio UI—using docker-compose.
The final architecture looks like this:
┌──────────────────────────┐
│ Docker Host │
│ │
│ ┌──────────────┐ │
│ │ PostgreSQL │◄───────┼── Loads HDB resale + MRT data on first run
│ └──────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────┐ │
│ │ Toolbox │◄───────┼── Local MCP agent for structured SQL/Geo tools
│ └──────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────┐ │
│ │ Backend API │ │
│ │ (FastAPI) │ │
│ └──────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────┐ │
│ │ Gradio UI │ │
│ └──────────────┘ │
└──────────────────────────┘
On first run, Postgres will:
- Create hdb_database
- Install PostGIS
- Create all required tables
- Auto-ingest all CSVs
The Dockerfile:
- Downloads the toolbox binary
- Runs it on port 5000
- Uses
tools.yamlinside the container
All agents call Toolbox via:
TOOLBOX_URL=http://localhost:5000The backend container exposes:
| Component | Port |
|---|---|
| FastAPI | 8000 |
| Gradio UI | 7860 |
| Toolbox API | 5000 |
The start.sh runs these in parallel:
- Toolbox
- FastAPI
- Gradio
Start environment:
docker-compose upAfter successful boot:
- Gradio UI → http://localhost:7860
- FastAPI docs → http://localhost:8000/docs
docker-compose down -vThis clears Postgres volumes and triggers CSV reload on next start.
You should be able to run:
Find cheapest 5-room flats near Punggol MRT under 600k
This project uses publicly available datasets from Singapore’s Housing & Development Board (HDB) provided via data.gov.sg under the Singapore Open Data Licence.
Please cite the following datasets if you use this project in research, reports, publications, or derivative works:
-
Housing & Development Board. (2016). Resale Flat Prices (Based on Approval Date), 1990–1999 (2024) [Dataset]. data.gov.sg. Retrieved December 7, 2025, from https://data.gov.sg/datasets/d_ebc5ab87086db484f88045b47411ebc5/view
-
Housing & Development Board. (2016). Resale Flat Prices (Based on Approval Date), 2000–Feb 2012 (2024) [Dataset]. data.gov.sg. Retrieved December 7, 2025, from https://data.gov.sg/datasets/d_43f493c6c50d54243cc1eab0df142d6a/view
-
Housing & Development Board. (2016). Resale Flat Prices (Based on Registration Date), From Mar 2012 to Dec 2014 (2024) [Dataset]. data.gov.sg. Retrieved December 7, 2025, from https://data.gov.sg/datasets/d_2d5ff9ea31397b66239f245f57751537/view
-
Housing & Development Board. (2017). Resale Flat Prices (Based on Registration Date), From Jan 2015 to Dec 2016 (2024) [Dataset]. data.gov.sg. Retrieved December 7, 2025, from https://data.gov.sg/datasets/d_ea9ed51da2787afaf8e51f827c304208/view
-
Housing & Development Board. (2021). Resale flat prices based on registration date from Jan-2017 onwards (2025) [Dataset]. data.gov.sg. Retrieved December 7, 2025, from https://data.gov.sg/datasets/d_8b84c4ee58e3cfc0ece0d773c8ca6abc/view
-
Housing & Development Board. (2018). HDB Property Information (2025) [Dataset]. data.gov.sg. Retrieved December 7, 2025 from https://data.gov.sg/datasets/d_17f5382f26140b1fdae0ba2ef6239d2f/view
-
National Parks Board. (2023). Parks (2025) [Dataset]. data.gov.sg. Retrieved December 7, 2025 from https://data.gov.sg/datasets/d_0542d48f0991541706b58059381a6eca/view
-
Ministry of Education. (2017). General information of schools (2025) [Dataset]. data.gov.sg. Retrieved December 7, 2025 from https://data.gov.sg/datasets/d_688b934f82c1059ed0a6993d2a829089/view
-
Land Transport Authority. (2019). LTA MRT Station Exit (GEOJSON) (2025) [Dataset]. data.gov.sg. Retrieved December 7, 2025 from https://data.gov.sg/datasets/d_b39d3a0871985372d7e1637193335da5/view
- Data is provided under the Singapore Open Data Licence.
- Users are permitted to reuse, modify, and redistribute these datasets.
- Proper attribution must be given, as included above.
- This project aggregates, normalizes, and enriches the datasets into a PostgreSQL schema suitable for agent-based geospatial queries.