Legal Compliance RAG Chatbot for Indian Startups
"In India, a startup can die not from lack of funding, but from a single regulatory misstep."
π‘ Inspiration
Every year, thousands of Indian startups are fined, shut down, or forced into costly legal restructuring β not because their ideas were bad, but because their founders simply didn't know the rules. A first-generation founder building a fintech app in Indore shouldn't need to pay βΉ5 lakh to a lawyer just to find out that their βΉ1 lakh digital wallet requires a full RBI PPI licence.
We saw this problem up close. Friends building lending apps, payment gateways, and SaaS products were constantly asking the same questions:
- "Do we need to register for GST?"
- "Can we accept money from a US investor without RBI approval?"
- "What happens if we skip KYC for small wallets?"
The answers exist β buried inside hundreds of RBI Master Directions, SEBI circulars, DPIIT notifications, and MCA filings. The knowledge is public but inaccessible.
We wanted to build something that could answer these questions instantly, accurately, and for free β a legal co-pilot powered by Retrieval-Augmented Generation (RAG), running entirely on Databricks Community Edition so anyone can replicate it.
ποΈ How We Built It
Architecture Overview
We designed a Medallion Architecture pipeline β the same pattern used in enterprise data lakes β adapted for a legal compliance use case.
RBI RSS Feed βββ
Startup India βββΌβββΆ π₯ BRONZE βββΆ π₯ SILVER βββΆ π₯ GOLD βββΆ π€ RAG Chatbot
SEBI / DPIIT βββ (Raw Text) (Chunks) (TF-IDF) (UI + LLM)
Stage 1 β Live Ingestion (Bronze Layer)
We fetch regulatory data from the RBI RSS feed (live circulars) and a curated mock of Startup India / SEBI / MCA policies. Every record is landed as a Delta table with full lineage metadata: record_id, source_url, ingested_at.
Stage 2 β Preprocessing (Silver Layer)
Raw policy text is messy β HTML tags, legal boilerplate, inconsistent whitespace. We built a PySpark UDF pipeline to clean and chunk each document into 200β400 character clauses:
strip_html() βββΆ remove_legal_noise() βββΆ normalise_whitespace() βββΆ chunk_text()
The chunker splits on legal structural markers β Section N, Clause N, numbered items β rather than blind word-count windows. Each chunk is then tagged with:
regulatory_body(RBI, SEBI, DPIIT, CBIC, MEITYβ¦)has_penaltyflag (boolean β does this clause mention fines or imprisonment?)
Stage 3 β Vectorisation (Gold Layer)
The core retrieval engine uses PySpark MLlib TF-IDF β entirely CPU-friendly, zero GPU required, runs on the free Databricks Community Edition tier.
The 5-stage pipeline:
$$ \text{tokens} \xrightarrow{\text{RegexTokenizer}} \text{filtered} \xrightarrow{\text{StopWordsRemover}} \vec{f} \xrightarrow{\text{HashingTF}} \vec{w} \xrightarrow{\text{IDF}} \hat{v} \xrightarrow{\text{Normalizer}_{L_2}} \vec{u} $$
TF-IDF formula for a term $t$ in document $d$ across corpus $D$:
$$ \text{TF-IDF}(t, d, D) = \underbrace{\left(1 + \log f_{t,d}\right)}{\text{sublinear TF}} \times \underbrace{\log \frac{|D|}{|{d \in D : t \in d}|}}{\text{IDF}} $$
We use sublinear TF (sublinear_tf=True) to prevent long documents from dominating purely because they repeat a term many times.
Each Gold vector is L2-normalised, meaning:
$$ \hat{v}_i = \frac{\vec{v}_i}{|\vec{v}_i|_2}, \quad |\hat{v}_i|_2 = 1 $$
This reduces cosine similarity to a simple dot product at query time:
$$ \text{sim}(q, d_i) = \hat{q} \cdot \hat{v}i = \sum{j=1}^{N} \hat{q}j \cdot \hat{v}{ij} $$
which is $O(N \cdot k)$ where $N$ is vocabulary size (8192) and $k$ is corpus size (~72 chunks) β fast enough for real-time retrieval on CE hardware.
Stage 4 β Query Expansion
The root cause of our biggest bug: TF-IDF only matches when query words literally appear in the policy corpus. "I need to buy a bank" has zero lexical overlap with "Banking Regulation Act, Section 22, RBI licence."
We solved this with a deterministic query expansion table β 50+ phrase mappings from plain English to Indian legal terminology:
$$ \mathcal{E}(q) = q \cup \bigcup_{\substack{p \in \mathcal{P} \ p \subseteq q}} \text{keywords}(p) $$
For example:
| User says | Expanded with |
|---|---|
"buy a bank" |
"banking licence RBI Banking Regulation Act acquisition Section 22" |
"digital wallet" |
"prepaid payment instrument PPI KYC limit RBI master direction" |
"without PAN" |
"PAN mandatory PMLA RBI penalty violation KYC full-KYC" |
After expansion, retrieval scores jump from $\approx 0.00$ to $\geq 0.15$ for relevant chunks.
Stage 5 β Retrieval with Diversity Filter
We retrieve top-$K$ chunks with two additional filters:
1. Relevance threshold β reject chunks below $\tau = 0.03$:
$$ \mathcal{R}_\tau = {d_i \mid \text{sim}(\hat{q}, \hat{v}_i) \geq \tau} $$
2. Diversity constraint β max 1 chunk per source document (prevents 3 chunks from the same law monopolising results):
$$ \mathcal{D} = \text{argmax}{S \subseteq \mathcal{R}\tau,\; |S|=K} \sum_{d \in S} \text{sim}(\hat{q}, \hat{v}_d) \quad \text{subject to} \quad |{d \in S : \text{src}(d) = r}| \leq 1 \;\; \forall r $$
Stage 6 β LLM Generation
The retrieved chunks become the context in a structured prompt:
System: You are a Senior Legal Advisor for Indian startups.
Context: [TOP-K POLICY CLAUSES with scores]
Question: Evaluate this decision: [USER INPUT]
Format: VERDICT | LEGAL ANALYSIS | RISKS | CORRECTIVE ACTIONS | CITED REGULATIONS
We support Groq (Llama3), OpenAI (GPT-4o mini), Google Gemini, and a smart mock LLM that gives unique scenario-specific answers without any API key β so the system works end-to-end even offline.
Stage 7 β MLflow Tracking
We log the full pipeline to Databricks MLflow:
| Metric | Value (our run) |
|---|---|
| Bronze records | 13 |
| Silver chunks | 72 |
| Gold vectors | 72 |
| TF-IDF features | 8,192 |
| Vector sparsity | ~0.0012 |
| Retrieval avg latency | 692ms |
| Retrieval P95 latency | 968ms |
Stage 8 β Streamlit Frontend
A standalone web app (streamlit run app.py) with:
- Sidebar: LLM provider selector, API key input, top-K and threshold sliders, 6 sample queries
- Left panel: Verdict badge, legal analysis, risks, corrective actions
- Right panel: Retrieved policy clauses with relevance scores, regulator tags, penalty flags, source links
- Debug expander: Top-5 retrieval scores + expanded query β fully transparent retrieval
π What We Learned
1. TF-IDF is surprisingly powerful for legal retrieval
We initially assumed we'd need sentence transformers or a vector database. But for a domain-specific corpus of ~70 chunks with precise legal terminology, TF-IDF + bigrams + L2 normalisation achieves excellent retrieval quality β and runs in under 1 second on a CPU with 15GB RAM.
The key insight: vocabulary gap is the real problem, not model capacity. Once we bridged the gap with query expansion, TF-IDF retrieved exactly the right clauses.
2. Medallion architecture is overkill for small corpora β and worth it anyway
For 72 chunks, we didn't strictly need three Delta layers. But the discipline of separating raw ingestion, transformation logic, and vectorisation paid off immediately when we needed to fix the retrieval bug β we could re-run only Notebook 04 without re-ingesting or re-vectorising anything.
3. Prompt engineering determines 80% of answer quality
The same retrieved chunks produce wildly different quality answers depending on how the prompt is structured. Our final prompt enforces:
- A fixed output schema (VERDICT β ANALYSIS β RISKS β ACTIONS β CITATIONS)
- Citation of specific section numbers
- Quantified penalty amounts
- Temperature $= 0.1$ for deterministic legal advice
4. The mock LLM is not a fallback β it's a feature
For a hackathon demo, the mock LLM that produces scenario-specific answers (bank acquisition vs. PPI wallet vs. FEMA vs. GST) without an API key means judges can experience the full system with zero setup. We learned to treat the offline path as a first-class citizen.
5. Streamlit's PATH gotcha on macOS
pip install streamlit doesn't mean streamlit is in your shell PATH. Always run python3 -m streamlit run app.py or add ~/Library/Python/3.9/bin to ~/.zshrc. Thirty minutes lost to this.
π§± Challenges We Faced
Challenge 1 β "Every query returns the same result"
Problem: Short vague queries like "I need to buy a bank" have zero lexical overlap with legal corpus text. Every chunk scores $\approx 0.000$, making the ranking essentially random and stable-sorted β always the same output.
Solution: Query expansion table mapping 50+ plain-English phrases to Indian legal keywords. After expansion, the same query becomes:
"buy a bank **banking licence RBI Banking Regulation Act acquisition approval Section 22"
...which now has strong overlap with 3 distinct policy chunks.
Challenge 2 β Delta Lake doesn't store MLlib SparseVectors
Problem: PySpark MLlib SparseVector columns cannot be written to Delta Lake natively β Spark throws a schema error.
Solution: Serialise each vector to a JSON string:
{"size": 8192, "indices": [12, 47, 203], "values": [0.42, 0.18, 0.31]}
and deserialise at query time. Adds ~2ms overhead per query. Completely invisible to the user.
Challenge 3 β Databricks Community Edition egress restrictions
Problem: The RBI RSS feed is sometimes blocked by CE's network egress policy. Notebook 01 would hang or throw a ConnectionError.
Solution: Built a rich mock dataset of 13 curated policies covering every major regulatory domain (RBI, SEBI, DPIIT, CBIC, MEITY, MCA, FEMA, CBDT) β detailed enough to produce real compliance verdicts β and made it the automatic fallback. The live RSS fetch is attempted first with a 15-second timeout.
Challenge 4 β Single-document chunk dominance
Problem: The RBI PPI Master Direction (our most detailed policy) had 8 chunks. For any fintech query, all top-3 results came from the same document β useful but not diverse.
Solution: Diversity constraint: max 1 chunk per record_id. This forced the retriever to pull from 3 distinct regulatory sources, making the context richer and the LLM analysis more comprehensive.
Challenge 5 β Free-tier memory on Databricks CE
Problem: Attempted to use CountVectorizer (keeps full vocabulary in driver memory) on the first iteration. On the 15GB CE cluster, this caused OOM errors with even a moderate corpus.
Solution: Switched to HashingTF β it maps tokens to hash buckets in $O(\text{tokens})$ time with no vocabulary materialised in memory. The hash space (numFeatures=8192) is a tunable hyperparameter logged to MLflow.
$$ \text{HashingTF}(t) = \text{hash}(t) \mod 2^{13} $$
Small risk of hash collision, but for a legal corpus with precise terminology, collisions are rare and have negligible impact on retrieval quality.
π What's Next
- Vector database upgrade: Replace TF-IDF with FAISS or ChromaDB for semantic (not just lexical) retrieval, enabling queries like "is my app breaking data protection rules?" without expansion
- Live RBI feed: Scheduled Databricks Job to ingest new circulars daily and invalidate affected Gold vectors
- Multi-turn conversation: Maintain session state so founders can ask follow-up questions like "what if we add Aadhaar KYC?"
- Hindi language support: Most rural startup founders communicate in Hindi β extend the query expansion table and prompt to handle Devanagari queries
- Confidence scoring: Surface retrieval uncertainty to the user β if the top score is below $0.05$, warn that the answer may be incomplete
π οΈ Tech Stack
| Layer | Technology |
|---|---|
| Data Platform | Databricks Community Edition |
| Storage | Delta Lake (DBFS) |
| Processing | PySpark 3.5, MLlib |
| Vectorisation | TF-IDF (HashingTF + IDF + L2 Normalizer) |
| LLM | Groq Llama3-8B / OpenAI GPT-4o mini / Google Gemini |
| Tracking | MLflow (metrics, charts, model registry) |
| Frontend | Streamlit + scikit-learn (local TF-IDF) |
| Data Sources | RBI RSS, Startup India, SEBI, MCA, MEITY (mock) |
π₯ Team
Built at the Databricks Hackathon 2024 by the LexAI team from IIT Indore.
"The law is a system. Systems can be queried."
β οΈ LexAI is an AI-generated informational tool. Always consult a qualified legal professional before acting on compliance advice.
Built With
- gemini-api
- groq
- llm
Log in or sign up for Devpost to join the conversation.