tanmayxchoudhary/urban-planning-rag

Visual RAG system for Indian urban planning documents

6

stars

79

commits

Python

primary language

Jun 22, 2026

updated

README

Urban Planning RAG πŸ™οΈ

Visual RAG system for Indian urban planning regulations

Phase 0 Status (2026-06-19): The April hybrid architecture described below (ColQwen2.5 + GTE + BM25 + Qdrant + RRF + Gemini rerank) is not the current live v1 path. It is stale and not used for v1. The real v1 runtime is the Tomoro Modal in-memory MaxSim retriever. See docs/tomoro_v1_runtime.md for the honest current state. Do not overclaim readiness.

A production-grade multimodal retrieval system that indexes planning documents (NBC, URDPFI, SWM Rules) as page images, embeds them with ColQwen2.5 visual encoders, and answers questions via Gemini 2.5 Flash with precise page citations.


What This Does

Ask questions like:

  • "What is the FSI for residential zones?"
  • "What are the parking requirements for commercial buildings?"
  • "What are the indicators of good governance?"

The system:

  1. Retrieves relevant pages using 3-channel parallel search (visual + text + sparse BM25)
  2. Fuses results with Reciprocal Rank Fusion (RRF k=60)
  3. Reranks with Gemini 2.5 Flash VLM cross-encoder
  4. Generates streaming answers with confidence levels and inline page citations

Architecture

Current v1 path: See docs/tomoro_v1_runtime.md. The detailed hybrid below is the April prototype (not live for v1). v1 is Tomoro 4B visual MaxSim (in-memory on Modal) over the 738-row legacy tensor. No Qdrant/hybrid for v1.

Embedding (April prototype β€” not v1)

  • Visual: vidore/colqwen2.5-v0.2 β€” 128-dim multi-vector per patch (ColPali-style late interaction)
  • Text: lightonai/GTE-ModernColBERT-v1 β€” 768-dim multi-vector per token (ModernBERT late interaction)

Vector DB (April prototype β€” not v1)

  • Qdrant with two collections:
    • pages_visual β€” ColQwen2.5 multi-vector with MAX_SIM comparator + INT8 scalar quantization
    • pages_text β€” GTE-ModernColBERT multi-vector (text + BM25 sparse)

Retrieval Pipeline (April prototype β€” not v1)

Query β†’ 3-channel parallel search (visual/text/sparse BM25)
      β†’ RRF k=60 fusion (top-20 per channel β†’ top-20 fused)
      β†’ Gemini 2.5 Flash VLM rerank (top-20 β†’ top-5)
  • Visual: ColQwen2.5 query encoding β†’ Qdrant ANN (200 pooled) β†’ MaxSim rerank β†’ top 20
  • Text: GTE-ModernColBERT query encoding β†’ Qdrant ANN (200 pooled) β†’ MaxSim rerank β†’ top 20
  • Sparse: BM25 query scoring β†’ Qdrant sparse vector search β†’ top 20
  • Fusion: RRF k=60 across all three channels
  • Rerank: Gemini 2.5 Flash cross-encoder scores page images β†’ top 5

Generation (April prototype β€” not v1)

  • Streaming Gemini 2.5 Flash client with SSE events
  • Confidence levels per claim, inline [k] citation markers
  • Grounded in retrieved page images sent alongside the query

API (April prototype β€” not v1)

  • FastAPI gateway on port 3100
  • SSE streaming for query responses
  • /v1/query β€” streaming query endpoint
  • /v1/healthz β€” health check
  • /metrics β€” Prometheus metrics

Web (April prototype β€” not v1)

  • Next.js 14 App Router (web/ directory)
  • Streaming query via Server-Sent Events
  • Citation lightbox (click [k] to view the source page)
  • Thumbs up/down feedback buttons

Observability (April prototype β€” not v1)

  • Langfuse tracing (per-query spans, OTel-compatible)
  • Prometheus metrics (qdrant_latency_seconds, gemini_cost_usd_total, faithfulness_p50)
  • Grafana dashboards (see infra/grafana/)

Evaluation (April prototype β€” not v1)

  • eval/smoke.jsonl β€” 25 hand-curated questions (CI gate on every PR)
  • eval/regression.jsonl β€” 106 questions including adversarial probes
  • RAGAS metrics: faithfulness, answer_relevance, context_precision, context_recall, answer_correctness
  • CI gates: recall@10 β‰₯ 0.85, faithfulness β‰₯ 0.85

Corpus Status

StatValue
Documents indexed8
Total pages743 PNG renders
Corpus ready to scaleNo β€” v1 uses legacy 738-row Tomoro tensor (Phase 0 recovery); April pipeline not ready for visual retrieval

Quick Start

1. Install dependencies

uv sync

2. Configure environment

Copy .env.example to .env and fill in required values:

# Google Gemini API (required)
GEMINI_API_KEY=your_gemini_api_key_here

# Qdrant vector database
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=your_qdrant_api_key_here   # optional for local

# Langfuse tracing (optional)
LANGFUSE_PUBLIC_KEY=your_langfuse_public_key
LANGFUSE_SECRET_KEY=your_langfuse_secret_key
LANGFUSE_HOST=https://cloud.langfuse.com

See .env.example for the full template.

3. Ingest documents

# Ingest a single PDF
python -m urban_rag.cli ingest ./path/to/document.pdf

# Ingest all PDFs in a directory
python -m urban_rag.cli ingest ./pdfs/

# Corpus management
python -m urban_rag.cli corpus list
python -m urban_rag.cli corpus stats

4. Query the corpus

# Streaming query via CLI
python -m urban_rag.cli query "What is FSI for residential zones?"

# Retrieve-only mode (no generation)
python -m urban_rag.cli query "parking requirements" --retrieve-only

# Control top-k candidates
python -m urban_rag.cli query "open space standards" --top-k 10

5. Run services

# Start the FastAPI gateway (port 3100)
uvicorn urban_rag.api.main:app --host 0.0.0.0 --port 3100

# Start the embed service (port 3102)
uvicorn urban_rag.embed.serve:app --host 0.0.0.0 --port 3102

# Start Qdrant (Docker)
docker run -d --name urban-rag-qdrant -p 3103:6333 qdrant/qdrant

# Start the Next.js web UI (port 3101)
cd web && npm run dev -- --port 3101

6. Run evaluation

# Smoke eval (25 questions, CI gate)
python -m src.eval run --dataset smoke

# Regression eval (106 questions, weekly)
python -m src.eval run --dataset regression

# Run tests
pytest tests/unit/ -v
pytest tests/integration/ -v

7. Lint and typecheck

ruff check src/urban_rag/
ruff format src/urban_rag/
pyright src/urban_rag/

Project Structure

urban-planning-rag/
β”œβ”€β”€ src/urban_rag/
β”‚   β”œβ”€β”€ api/              # FastAPI gateway (main.py, /v1/query streaming endpoint)
β”‚   β”œβ”€β”€ embed/            # ColQwen2.5 + GTE-ModernColBERT encoder loaders
β”‚   β”‚   β”œβ”€β”€ colqwen.py    # Visual embedding model
β”‚   β”‚   β”œβ”€β”€ text_encoder.py  # Text embedding model
β”‚   β”‚   └── serve.py      # Embed service (uvicorn, port 3102)
β”‚   β”œβ”€β”€ index/            # Qdrant batch indexers (visual, text, BM25 sparse)
β”‚   β”‚   β”œβ”€β”€ batch.py      # Visual index (ColQwen2.5 β†’ Qdrant pages_visual)
β”‚   β”‚   β”œβ”€β”€ text_index.py # Text index (GTE-ModernColBERT β†’ Qdrant pages_text)
β”‚   β”‚   └── sparse.py     # Sparse BM25 indexer
β”‚   β”œβ”€β”€ retrieve/         # Query execution
β”‚   β”‚   β”œβ”€β”€ visual.py     # Visual channel (Qdrant ANN + MaxSim)
β”‚   β”‚   β”œβ”€β”€ text.py       # Text channel (GTE-ModernColBERT + MaxSim)
β”‚   β”‚   β”œβ”€β”€ sparse.py     # Sparse BM25 channel (Qdrant native sparse)
β”‚   β”‚   β”œβ”€β”€ rerank.py     # Gemini 2.5 Flash VLM cross-encoder rerank
β”‚   β”‚   └── orchestrator.py  # 3-channel RRF fusion + orchestrates retrieval
β”‚   β”œβ”€β”€ generate/         # Gemini streaming generation
β”‚   β”‚   β”œβ”€β”€ gemini.py     # Streaming client with SSE parsing
β”‚   β”‚   β”œβ”€β”€ orchestrator.py  # Grounded generation with citations
β”‚   β”‚   └── prompts.py   # Prompt templates (fast/deep modes)
β”‚   β”œβ”€β”€ ingest/           # PDF parse + render ingest pipeline
β”‚   β”‚   β”œβ”€β”€ load.py       # PDF validation and hashing
β”‚   β”‚   β”œβ”€β”€ parse.py      # Docling/Marker markdown extraction
β”‚   β”‚   β”œβ”€β”€ classify.py   # Per-page DPI classifier (text vs visual)
β”‚   β”‚   β”œβ”€β”€ render.py     # PDF β†’ PNG at adaptive DPI (100/250)
β”‚   β”‚   β”œβ”€β”€ chunk.py      # Text chunking with overlap
β”‚   β”‚   └── sections.py   # Section boundary detection
β”‚   β”œβ”€β”€ eval/             # RAGAS metrics + smoke/regression CI gates
β”‚   β”‚   └── metrics/
β”‚   β”‚       └── ragas_wrapper.py  # Pinned judge model, reproducible scores
β”‚   β”œβ”€β”€ telemetry/        # Observability
β”‚   β”‚   β”œβ”€β”€ tracing.py    # Langfuse OTel spans
β”‚   β”‚   └── metrics.py    # Prometheus gauges and histograms
β”‚   β”œβ”€β”€ cli/              # Typer CLI commands (ingest, corpus, query)
β”‚   └── common/           # Settings, types, logging, errors
β”œβ”€β”€ web/                  # Next.js 14 App Router frontend
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   └── page.tsx     # Main query UI
β”‚   └── lib/
β”‚       └── api.ts        # SSE streaming client
β”œβ”€β”€ eval/
β”‚   β”œβ”€β”€ smoke.jsonl       # 25 CI gate questions
β”‚   └── regression.jsonl  # 106 regression questions
β”œβ”€β”€ infra/
β”‚   β”œβ”€β”€ lightning/        # Lightning AI GPU deployment scripts
β”‚   └── grafana/         # Dashboard configs
└── services.yaml        # Service commands manifest (single source of truth)

Deferred Items (Require Credentials)

The following are not yet deployed β€” pending infrastructure credentials:

  • GPU embed service on Lightning AI Studios (LitServe) β€” deploy with infra/lightning/deploy-embed.sh
  • Qdrant Cloud production cluster β€” update QDRANT_URL and QDRANT_API_KEY in .env
  • Vercel web deployment β€” see web/VERCEL_DEPLOY.md

Why Visual RAG?

Planning documents contain tables, diagrams, flowcharts, and color-coded maps. Traditional OCR destroys spatial layout and visual context. This system embeds entire page images as multi-vector representations, preserving all visual information for retrieval.


Citation

@software{choudhary2026urbanrag,
  author = {Choudhary, Tanmay},
  title = {Urban Planning RAG: Visual Retrieval for Indian Planning Documents},
  year = {2026},
  url = {https://github.com/tanmayxchoudhary/urban-planning-rag}
}

Contributors

tanmayxchoudhary/urban-planning-rag

Visual RAG system for Indian urban planning documents

6

stars

79

commits

Python

primary language

Jun 22, 2026

updated

README

Urban Planning RAG πŸ™οΈ

Visual RAG system for Indian urban planning regulations

Phase 0 Status (2026-06-19): The April hybrid architecture described below (ColQwen2.5 + GTE + BM25 + Qdrant + RRF + Gemini rerank) is not the current live v1 path. It is stale and not used for v1. The real v1 runtime is the Tomoro Modal in-memory MaxSim retriever. See docs/tomoro_v1_runtime.md for the honest current state. Do not overclaim readiness.

A production-grade multimodal retrieval system that indexes planning documents (NBC, URDPFI, SWM Rules) as page images, embeds them with ColQwen2.5 visual encoders, and answers questions via Gemini 2.5 Flash with precise page citations.


What This Does

Ask questions like:

  • "What is the FSI for residential zones?"
  • "What are the parking requirements for commercial buildings?"
  • "What are the indicators of good governance?"

The system:

  1. Retrieves relevant pages using 3-channel parallel search (visual + text + sparse BM25)
  2. Fuses results with Reciprocal Rank Fusion (RRF k=60)
  3. Reranks with Gemini 2.5 Flash VLM cross-encoder
  4. Generates streaming answers with confidence levels and inline page citations

Architecture

Current v1 path: See docs/tomoro_v1_runtime.md. The detailed hybrid below is the April prototype (not live for v1). v1 is Tomoro 4B visual MaxSim (in-memory on Modal) over the 738-row legacy tensor. No Qdrant/hybrid for v1.

Embedding (April prototype β€” not v1)

  • Visual: vidore/colqwen2.5-v0.2 β€” 128-dim multi-vector per patch (ColPali-style late interaction)
  • Text: lightonai/GTE-ModernColBERT-v1 β€” 768-dim multi-vector per token (ModernBERT late interaction)

Vector DB (April prototype β€” not v1)

  • Qdrant with two collections:
    • pages_visual β€” ColQwen2.5 multi-vector with MAX_SIM comparator + INT8 scalar quantization
    • pages_text β€” GTE-ModernColBERT multi-vector (text + BM25 sparse)

Retrieval Pipeline (April prototype β€” not v1)

Query β†’ 3-channel parallel search (visual/text/sparse BM25)
      β†’ RRF k=60 fusion (top-20 per channel β†’ top-20 fused)
      β†’ Gemini 2.5 Flash VLM rerank (top-20 β†’ top-5)
  • Visual: ColQwen2.5 query encoding β†’ Qdrant ANN (200 pooled) β†’ MaxSim rerank β†’ top 20
  • Text: GTE-ModernColBERT query encoding β†’ Qdrant ANN (200 pooled) β†’ MaxSim rerank β†’ top 20
  • Sparse: BM25 query scoring β†’ Qdrant sparse vector search β†’ top 20
  • Fusion: RRF k=60 across all three channels
  • Rerank: Gemini 2.5 Flash cross-encoder scores page images β†’ top 5

Generation (April prototype β€” not v1)

  • Streaming Gemini 2.5 Flash client with SSE events
  • Confidence levels per claim, inline [k] citation markers
  • Grounded in retrieved page images sent alongside the query

API (April prototype β€” not v1)

  • FastAPI gateway on port 3100
  • SSE streaming for query responses
  • /v1/query β€” streaming query endpoint
  • /v1/healthz β€” health check
  • /metrics β€” Prometheus metrics

Web (April prototype β€” not v1)

  • Next.js 14 App Router (web/ directory)
  • Streaming query via Server-Sent Events
  • Citation lightbox (click [k] to view the source page)
  • Thumbs up/down feedback buttons

Observability (April prototype β€” not v1)

  • Langfuse tracing (per-query spans, OTel-compatible)
  • Prometheus metrics (qdrant_latency_seconds, gemini_cost_usd_total, faithfulness_p50)
  • Grafana dashboards (see infra/grafana/)

Evaluation (April prototype β€” not v1)

  • eval/smoke.jsonl β€” 25 hand-curated questions (CI gate on every PR)
  • eval/regression.jsonl β€” 106 questions including adversarial probes
  • RAGAS metrics: faithfulness, answer_relevance, context_precision, context_recall, answer_correctness
  • CI gates: recall@10 β‰₯ 0.85, faithfulness β‰₯ 0.85

Corpus Status

StatValue
Documents indexed8
Total pages743 PNG renders
Corpus ready to scaleNo β€” v1 uses legacy 738-row Tomoro tensor (Phase 0 recovery); April pipeline not ready for visual retrieval

Quick Start

1. Install dependencies

uv sync

2. Configure environment

Copy .env.example to .env and fill in required values:

# Google Gemini API (required)
GEMINI_API_KEY=your_gemini_api_key_here

# Qdrant vector database
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=your_qdrant_api_key_here   # optional for local

# Langfuse tracing (optional)
LANGFUSE_PUBLIC_KEY=your_langfuse_public_key
LANGFUSE_SECRET_KEY=your_langfuse_secret_key
LANGFUSE_HOST=https://cloud.langfuse.com

See .env.example for the full template.

3. Ingest documents

# Ingest a single PDF
python -m urban_rag.cli ingest ./path/to/document.pdf

# Ingest all PDFs in a directory
python -m urban_rag.cli ingest ./pdfs/

# Corpus management
python -m urban_rag.cli corpus list
python -m urban_rag.cli corpus stats

4. Query the corpus

# Streaming query via CLI
python -m urban_rag.cli query "What is FSI for residential zones?"

# Retrieve-only mode (no generation)
python -m urban_rag.cli query "parking requirements" --retrieve-only

# Control top-k candidates
python -m urban_rag.cli query "open space standards" --top-k 10

5. Run services

# Start the FastAPI gateway (port 3100)
uvicorn urban_rag.api.main:app --host 0.0.0.0 --port 3100

# Start the embed service (port 3102)
uvicorn urban_rag.embed.serve:app --host 0.0.0.0 --port 3102

# Start Qdrant (Docker)
docker run -d --name urban-rag-qdrant -p 3103:6333 qdrant/qdrant

# Start the Next.js web UI (port 3101)
cd web && npm run dev -- --port 3101

6. Run evaluation

# Smoke eval (25 questions, CI gate)
python -m src.eval run --dataset smoke

# Regression eval (106 questions, weekly)
python -m src.eval run --dataset regression

# Run tests
pytest tests/unit/ -v
pytest tests/integration/ -v

7. Lint and typecheck

ruff check src/urban_rag/
ruff format src/urban_rag/
pyright src/urban_rag/

Project Structure

urban-planning-rag/
β”œβ”€β”€ src/urban_rag/
β”‚   β”œβ”€β”€ api/              # FastAPI gateway (main.py, /v1/query streaming endpoint)
β”‚   β”œβ”€β”€ embed/            # ColQwen2.5 + GTE-ModernColBERT encoder loaders
β”‚   β”‚   β”œβ”€β”€ colqwen.py    # Visual embedding model
β”‚   β”‚   β”œβ”€β”€ text_encoder.py  # Text embedding model
β”‚   β”‚   └── serve.py      # Embed service (uvicorn, port 3102)
β”‚   β”œβ”€β”€ index/            # Qdrant batch indexers (visual, text, BM25 sparse)
β”‚   β”‚   β”œβ”€β”€ batch.py      # Visual index (ColQwen2.5 β†’ Qdrant pages_visual)
β”‚   β”‚   β”œβ”€β”€ text_index.py # Text index (GTE-ModernColBERT β†’ Qdrant pages_text)
β”‚   β”‚   └── sparse.py     # Sparse BM25 indexer
β”‚   β”œβ”€β”€ retrieve/         # Query execution
β”‚   β”‚   β”œβ”€β”€ visual.py     # Visual channel (Qdrant ANN + MaxSim)
β”‚   β”‚   β”œβ”€β”€ text.py       # Text channel (GTE-ModernColBERT + MaxSim)
β”‚   β”‚   β”œβ”€β”€ sparse.py     # Sparse BM25 channel (Qdrant native sparse)
β”‚   β”‚   β”œβ”€β”€ rerank.py     # Gemini 2.5 Flash VLM cross-encoder rerank
β”‚   β”‚   └── orchestrator.py  # 3-channel RRF fusion + orchestrates retrieval
β”‚   β”œβ”€β”€ generate/         # Gemini streaming generation
β”‚   β”‚   β”œβ”€β”€ gemini.py     # Streaming client with SSE parsing
β”‚   β”‚   β”œβ”€β”€ orchestrator.py  # Grounded generation with citations
β”‚   β”‚   └── prompts.py   # Prompt templates (fast/deep modes)
β”‚   β”œβ”€β”€ ingest/           # PDF parse + render ingest pipeline
β”‚   β”‚   β”œβ”€β”€ load.py       # PDF validation and hashing
β”‚   β”‚   β”œβ”€β”€ parse.py      # Docling/Marker markdown extraction
β”‚   β”‚   β”œβ”€β”€ classify.py   # Per-page DPI classifier (text vs visual)
β”‚   β”‚   β”œβ”€β”€ render.py     # PDF β†’ PNG at adaptive DPI (100/250)
β”‚   β”‚   β”œβ”€β”€ chunk.py      # Text chunking with overlap
β”‚   β”‚   └── sections.py   # Section boundary detection
β”‚   β”œβ”€β”€ eval/             # RAGAS metrics + smoke/regression CI gates
β”‚   β”‚   └── metrics/
β”‚   β”‚       └── ragas_wrapper.py  # Pinned judge model, reproducible scores
β”‚   β”œβ”€β”€ telemetry/        # Observability
β”‚   β”‚   β”œβ”€β”€ tracing.py    # Langfuse OTel spans
β”‚   β”‚   └── metrics.py    # Prometheus gauges and histograms
β”‚   β”œβ”€β”€ cli/              # Typer CLI commands (ingest, corpus, query)
β”‚   └── common/           # Settings, types, logging, errors
β”œβ”€β”€ web/                  # Next.js 14 App Router frontend
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   └── page.tsx     # Main query UI
β”‚   └── lib/
β”‚       └── api.ts        # SSE streaming client
β”œβ”€β”€ eval/
β”‚   β”œβ”€β”€ smoke.jsonl       # 25 CI gate questions
β”‚   └── regression.jsonl  # 106 regression questions
β”œβ”€β”€ infra/
β”‚   β”œβ”€β”€ lightning/        # Lightning AI GPU deployment scripts
β”‚   └── grafana/         # Dashboard configs
└── services.yaml        # Service commands manifest (single source of truth)

Deferred Items (Require Credentials)

The following are not yet deployed β€” pending infrastructure credentials:

  • GPU embed service on Lightning AI Studios (LitServe) β€” deploy with infra/lightning/deploy-embed.sh
  • Qdrant Cloud production cluster β€” update QDRANT_URL and QDRANT_API_KEY in .env
  • Vercel web deployment β€” see web/VERCEL_DEPLOY.md

Why Visual RAG?

Planning documents contain tables, diagrams, flowcharts, and color-coded maps. Traditional OCR destroys spatial layout and visual context. This system embeds entire page images as multi-vector representations, preserving all visual information for retrieval.


Citation

@software{choudhary2026urbanrag,
  author = {Choudhary, Tanmay},
  title = {Urban Planning RAG: Visual Retrieval for Indian Planning Documents},
  year = {2026},
  url = {https://github.com/tanmayxchoudhary/urban-planning-rag}
}

Contributors

Languages

Python

92.7%

TypeScript

6.5%