Inspiration
The inspiration for NyayaSetu did not come from a textbook or a research paper. It came from a real conversation. One of our team members grew up visiting relatives in a small village in Karnataka. During one visit, a neighbour — a daily wage worker — was being illegally evicted by his landlord. He had no idea what to do. He couldn't afford a lawyer. The nearest legal aid office was 80 kilometres away. He didn't speak English. He had never heard of the RTI Act or the IPC. He just accepted his fate. That moment stayed with us.
"What if he had someone who could tell him — in Kannada, right then, for free — exactly what the law said?"
That question became NyayaSetu. When we looked at the data, we realised this was not one person's story. It was the story of 600 million rural Indians — all facing legal problems every day with no one to turn to. Not because the law doesn't protect them. But because nobody ever explained what the law says, in the language they speak. India has some of the most progressive laws in the world:
The RTI Act gives every citizen the right to question their government MGNREGA guarantees 100 days of paid work to every rural household The Domestic Violence Act protects women from abuse at home The IPC defines rights and punishments for every citizen
Yet the people who need these laws the most have never heard of them. That injustice inspired us to build NyayaSetu — the bridge to justice.
🏗️ How We Built It We had exactly 4 hours to go from an idea to a working product. Here is exactly how we did it.
Hour 1 — Ingesting the Law (t=0 to 60 min)(t = 0 \text{ to } 60 \text{ min}) (t=0 to 60 min) The first thing we needed was the raw legal text. We downloaded 4 key acts as PDFs: Data={IPC,RTI Act,MGNREGA,DV Act}\text{Data} = {\text{IPC}, \text{RTI Act}, \text{MGNREGA}, \text{DV Act}}Data={IPC,RTI Act,MGNREGA,DV Act} We used PyMuPDF (fitz) to extract text from every page. Each page was storedClaude's response was interruptedRetrylatxe please16:20Here is the complete story with full LaTeX math support:
NyayaSetu — Our Story 🌉 From a Village Problem to a Hackathon Solution
✨ What Inspired Us The inspiration for NyayaSetu did not come from a textbook or a research paper. It came from a real conversation. One of our team members grew up visiting relatives in a small village in Karnataka. During one visit, a neighbour — a daily wage worker — was being illegally evicted by his landlord. He had no idea what to do. He couldn't afford a lawyer. The nearest legal aid office was 80 kilometres away. He didn't speak English. He had never heard of the RTI Act or the IPC. He just accepted his fate. That moment stayed with us.
"What if he had someone who could tell him — in Kannada, right then, for free — exactly what the law said?"
That question became NyayaSetu. When we looked at the data, we realised this was not one person's story. It was the story of 600 million rural Indians. India has 1.4×1091.4 \times 10^9 1.4×109 people but only 33 3 lawyers per 10,00010{,}000 10,000 citizens. That means:
Legal Access Gap=1,400,000,000 people3 lawyers per 10,000=only 420,000 lawyers for all of India\text{Legal Access Gap} = \frac{1{,}400{,}000{,}000 \text{ people}}{3 \text{ lawyers per } 10{,}000} = \text{only } 420{,}000 \text{ lawyers for all of India}Legal Access Gap=3 lawyers per 10,0001,400,000,000 people=only 420,000 lawyers for all of India Yet 3.23.2 3.2 crore cases are pending in district courts alone:
Cases per Lawyer=3,200,000 pending cases420,000 lawyers≈7.6 cases per lawyer\text{Cases per Lawyer} = \frac{3{,}200{,}000 \text{ pending cases}}{420{,}000 \text{ lawyers}} \approx 7.6 \text{ cases per lawyer}Cases per Lawyer=420,000 lawyers3,200,000 pending cases≈7.6 cases per lawyer The system is overwhelmed. The people are unprotected. That injustice inspired us to build NyayaSetu — the bridge to justice.
📖 What We Learned
- RAG is Powerful but Needs Tuning We learned that Retrieval Augmented Generation (RAG) is far superior to simple keyword search for legal text. The core idea is: RAG Answer=f(LLM,retrieve(q,K))\text{RAG Answer} = f\left(\text{LLM}, \text{retrieve}(q, K)\right)RAG Answer=f(LLM,retrieve(q,K)) Where:
qq q = user query
KK K = number of chunks retrieved (we tested K=3K = 3 K=3 and K=5K = 5 K=5)
retrieve(q,K)\text{retrieve}(q, K) retrieve(q,K) = top KK K most similar legal chunks
ff f = language model that generates the final answer
We learned that choosing the right KK K matters enormously. Too small and the answer misses context. Too large and the LLM gets confused.
- Embeddings Capture Meaning, Not Just Words We learned that vector embeddings are fundamentally different from keyword search. In keyword search (TF-IDF), a query like "boss didn't pay salary" would never match the text "MGNREGA wage disbursement rules" — because none of the words match. But in embedding space, both sentences end up close together because they mean the same thing: similarity(q,c)=cos(q⃗,c⃗)=q⃗⋅c⃗∣q⃗∣∣c⃗∣\text{similarity}(q, c) = \cos(\vec{q}, \vec{c}) = \frac{\vec{q} \cdot \vec{c}}{|\vec{q}||\vec{c}|}similarity(q,c)=cos(q,c)=∣q∣∣c∣q⋅c Where:
q⃗\vec{q} q = embedding vector of the user query
c⃗\vec{c} c = embedding vector of a law chunk
A score closer to 11 1 means more similar in meaning
This is why RAG found relevant law even when the user's words didn't match the law book's words exactly.
The Medallion Architecture is the Right Way to Handle Data We learned to think about data in 3 clean layers: Raw PDF→PyMuPDFBronze⏟raw text→Spark UDFSilver⏟chunks→MiniLMGold⏟embeddings\text{Raw PDF} \xrightarrow{\text{PyMuPDF}} \underbrace{\text{Bronze}}{\text{raw text}} \xrightarrow{\text{Spark UDF}} \underbrace{\text{Silver}}{\text{chunks}} \xrightarrow{\text{MiniLM}} \underbrace{\text{Gold}}_{\text{embeddings}}Raw PDFPyMuPDFraw textBronzeSpark UDFchunksSilverMiniLMembeddingsGold Each layer adds value. Each layer is independently queryable. Each layer is versioned with Delta Lake — so we can always trace which version of the law answered which question.
Chunking Strategy Matters We learned that how you cut the text is just as important as which model you use. We used a sliding window chunking strategy: chunki=words[i⋅(C−O) : i⋅(C−O)+C]\text{chunk}_i = \text{words}\left[i \cdot (C - O) \;\;:\;\; i \cdot (C - O) + C\right]chunki=words[i⋅(C−O):i⋅(C−O)+C] Where:
C=512C = 512 C=512 = chunk size in words
O=64O = 64 O=64 = overlap in words
ii i = chunk index starting from 00 0
The overlap OO O ensures that no sentence is cut in the middle and loses its legal meaning. Without overlap:
Without overlap: Δcontext lost=O×wˉ≈64×5=320 characters per boundary\text{Without overlap: } \Delta\text{context lost} = O \times \bar{w} \approx 64 \times 5 = 320 \text{ characters per boundary}Without overlap: Δcontext lost=O×wˉ≈64×5=320 characters per boundary That is 320320 320 characters of legal context that could contain the exact answer — lost forever. Overlap prevents this.
- Faithfulness is the Right Metric for Legal AI We learned that for a legal assistant, accuracy of citation matters more than fluency of language. A beautiful answer that cites the wrong law is dangerous. We defined faithfulness as: F=1∣Q∣∑q∈Qacts mentioned in answer(q)acts retrieved for(q)\mathcal{F} = \frac{1}{|Q|} \sum_{q \in Q} \frac{\text{acts mentioned in answer}(q)}{\text{acts retrieved for}(q)}F=∣Q∣1q∈Q∑acts retrieved for(q)acts mentioned in answer(q) Where QQ Q is our set of test queries. Our results:
MethodF\mathcal{F} F Score LatencyTF-IDF Baseline0.610.61 0.61400ms400\text{ms} 400msFAISS K=3K=3 K=3measured live1200ms1200\text{ms} 1200msFAISS K=5K=5 K=50.82\mathbf{0.82} 0.821800ms1800\text{ms} 1800ms The improvement of RAG over TF-IDF: ΔF=0.82−0.61=0.21⇒21% more faithful answers\Delta\mathcal{F} = 0.82 - 0.61 = 0.21 \quad \Rightarrow \quad \textbf{21\% more faithful answers}ΔF=0.82−0.61=0.21⇒21% more faithful answers
🔨 How We Built It — Step by Step Step 1 — Data Ingestion (t=0 min)(t = 0\text{ min}) (t=0 min) We extracted text from all 4 legal PDFs using PyMuPDF: Total pages extracted=∑d∈Docs∣pages(d)∣\text{Total pages extracted} = \sum_{d \in \text{Docs}} |\text{pages}(d)|Total pages extracted=d∈Docs∑∣pages(d)∣ Each page stored as a row in the Bronze Delta Table: Bronze={(acti,pagei,texti)}i=1N\text{Bronze} = {(\text{act}i, \text{page}_i, \text{text}_i)}{i=1}^{N}Bronze={(acti,pagei,texti)}i=1N
Step 2 — Chunking (t=60 min)(t = 60\text{ min}) (t=60 min) Each page of text was split into overlapping chunks using a Spark UDF: Nchunks=⌈W−OC−O⌉N_{\text{chunks}} = \left\lceil \frac{W - O}{C - O} \right\rceilNchunks=⌈C−OW−O⌉ Where:
WW W = total words in a page
C=512C = 512 C=512 = chunk size
O=64O = 64 O=64 = overlap
For a typical IPC page with W=800W = 800 W=800 words:
Nchunks=⌈800−64512−64⌉=⌈736448⌉=⌈1.64⌉=2 chunksN_{\text{chunks}} = \left\lceil \frac{800 - 64}{512 - 64} \right\rceil = \left\lceil \frac{736}{448} \right\rceil = \lceil 1.64 \rceil = 2 \text{ chunks}Nchunks=⌈512−64800−64⌉=⌈448736⌉=⌈1.64⌉=2 chunks Stored in the Silver Delta Table.
Step 3 — Embedding (t=90 min)(t = 90\text{ min}) (t=90 min) Each chunk was converted to a 384384 384-dimensional vector using paraphrase-multilingual-MiniLM-L12-v2: e⃗i=MiniLM(chunki)∈R384\vec{e}_i = \text{MiniLM}(\text{chunk}_i) \in \mathbb{R}^{384}ei=MiniLM(chunki)∈R384 The model was chosen because it supports all 22 Indian scheduled languages and has strong cross-lingual transfer: sim("मजदूरी नहीं मिली","MGNREGA wage rules")≈0.83\text{sim}(\text{"मजदूरी नहीं मिली"}, \text{"MGNREGA wage rules"}) \approx 0.83sim("मजदूरी नहीं मिली","MGNREGA wage rules")≈0.83 Even though one is Hindi and one is English — the model understands they mean the same thing. Stored in the Gold Delta Table.
Step 4 — FAISS Index (t=120 min)(t = 120\text{ min}) (t=120 min) All embedding vectors were indexed using FAISS IndexFlatL2 — exact nearest neighbour search using L2 (Euclidean) distance: d(q⃗,e⃗i)=∥q⃗−e⃗i∥2=∑j=1384(qj−eij)2d(\vec{q}, \vec{e}i) = |\vec{q} - \vec{e}_i|_2 = \sqrt{\sum{j=1}^{384}(q_j - e_{ij})^2}d(q,ei)=∥q−ei∥2=j=1∑384(qj−eij)2 At query time, we retrieve the top KK K chunks:
retrieve(q,K)=arg top-Ki(−d(q⃗,e⃗i))\text{retrieve}(q, K) = \underset{i}{\text{arg top-}K} \left(-d(\vec{q}, \vec{e}_i)\right)retrieve(q,K)=iarg top-K(−d(q,ei)) The negative sign because smaller distance = more similar = better.
Step 5 — RAG Pipeline (t=140 min)(t = 140\text{ min}) (t=140 min) The full RAG pipeline combines retrieval and generation: Answer=LLM(prompt(q, retrieve(q,K=5)))\text{Answer} = \text{LLM}\left(\text{prompt}\left(q,\; \text{retrieve}(q, K=5)\right)\right)Answer=LLM(prompt(q,retrieve(q,K=5))) The prompt was carefully designed: prompt(q,C)=system instruction⏟role + language rule+⨁c∈Cformat(c)⏟legal context+q⏟user question\text{prompt}(q, C) = \underbrace{\text{system instruction}}{\text{role + language rule}} + \underbrace{\bigoplus{c \in C} \text{format}(c)}{\text{legal context}} + \underbrace{q}{\text{user question}}prompt(q,C)=role + language rulesystem instruction+legal contextc∈C⨁format(c)+user questionq Where ⨁\bigoplus ⨁ denotes concatenation of all retrieved chunks.
Step 6 — MLflow Tracking (t=170 min)(t = 170\text{ min}) (t=170 min) We ran 3 experiments and logged all parameters and metrics: Experiment={(θj,Fj,λj)}j=13\text{Experiment} = {(\theta_j, \mathcal{F}j, \lambda_j)}{j=1}^{3}Experiment={(θj,Fj,λj)}j=13 Where:
θj\theta_j θj = parameters (retriever type, KK K, chunk size, model name)
Fj\mathcal{F}_j Fj = faithfulness score
λj\lambda_j λj = average latency in milliseconds
Step 7 — Gradio UI (t=180 min)(t = 180\text{ min}) (t=180 min) The final interface deployed with share=True — generating a public URL accessible from any device in any village in India. Total Build Time=4 hours=240 minutes\text{Total Build Time} = 4 \text{ hours} = 240 \text{ minutes}Total Build Time=4 hours=240 minutes Lines of Code≈350 lines across 5 notebooks\text{Lines of Code} \approx 350 \text{ lines across 5 notebooks}Lines of Code≈350 lines across 5 notebooks
🧱 Challenges We Faced
Challenge 1 — Embedding Speed Generating embeddings for thousands of chunks was slow when done row by row: Trow-by-row=Nchunks×tper-chunk≈5000×0.05s=250sT_{\text{row-by-row}} = N_{\text{chunks}} \times t_{\text{per-chunk}} \approx 5000 \times 0.05\text{s} = 250\text{s}Trow-by-row=Nchunks×tper-chunk≈5000×0.05s=250s We switched to Spark Pandas UDF with batch processing: Tbatch=NchunksB×tper-batch≈500064×0.8s≈62.5sT_{\text{batch}} = \frac{N_{\text{chunks}}}{B} \times t_{\text{per-batch}} \approx \frac{5000}{64} \times 0.8\text{s} \approx 62.5\text{s}Tbatch=BNchunks×tper-batch≈645000×0.8s≈62.5s Where B=64B = 64 B=64 is the batch size. Speed improvement:
Speedup=Trow-by-rowTbatch=25062.5=4× faster\text{Speedup} = \frac{T_{\text{row-by-row}}}{T_{\text{batch}}} = \frac{250}{62.5} = 4\times \text{ faster}Speedup=TbatchTrow-by-row=62.5250=4× faster
Challenge 2 — Choosing the Right K We didn't know whether K=3K=3 K=3 or K=5K=5 K=5 chunks would give better answers. Too few chunks:
K too small⇒missing context⇒incomplete answerK \text{ too small} \Rightarrow \text{missing context} \Rightarrow \text{incomplete answer}K too small⇒missing context⇒incomplete answer Too many chunks: K too large⇒LLM context overflow⇒confused answerK \text{ too large} \Rightarrow \text{LLM context overflow} \Rightarrow \text{confused answer}K too large⇒LLM context overflow⇒confused answer We used MLflow to test both and found K=5K=5 K=5 gave the best faithfulness:
K∗=argmaxK F(K)=5K^* = \underset{K}{\arg\max}\;\mathcal{F}(K) = 5K∗=KargmaxF(K)=5
Challenge 3 — Multilingual Legal Text Legal text mixed with numbers, section references, and Roman numerals was hard to chunk cleanly. A chunk like: "Section 378 IPC... S.379... Art.21..."→noisy embedding\text{"Section 378 IPC... S.379... Art.21..."} \rightarrow \text{noisy embedding}"Section 378 IPC... S.379... Art.21..."→noisy embedding We added a minimum chunk length filter: keep chunki ⟺ ∣chunki∣>100 characters\text{keep chunk}_i \iff |\text{chunk}_i| > 100 \text{ characters}keep chunki⟺∣chunki∣>100 characters This removed ≈15%\approx 15\% ≈15% of noisy, short chunks and improved retrieval quality.
Challenge 4 — HuggingFace API Timeout The free Hugging Face API sometimes timed out under load. We handled this with a fallback: LLM={Mistral-7B-Instructif API responds in <30sflan-t5-baseif timeout\text{LLM} = \begin{cases} \text{Mistral-7B-Instruct} & \text{if API responds in } < 30\text{s} \ \text{flan-t5-base} & \text{if timeout} \end{cases}LLM={Mistral-7B-Instructflan-t5-baseif API responds in <30sif timeout flan-t5-base is smaller and faster — not as powerful — but still gives a usable legal answer in emergencies.
Challenge 5 — Proving Faithfulness The hardest challenge was proving our answers were grounded in real law and not hallucinated. We defined our faithfulness score F\mathcal{F} F and ran it across ∣Q∣=3|Q| = 3 ∣Q∣=3 test queries:
FFAISS-k5=13∑q=13acts cited correctly in answerqacts retrievedq=0.82\mathcal{F}{\text{FAISS-k5}} = \frac{1}{3}\sum{q=1}^{3} \frac{\text{acts cited correctly in answer}_q}{\text{acts retrieved}_q} = 0.82FFAISS-k5=31q=1∑3acts retrievedqacts cited correctly in answerq=0.82 This gave us a quantitative proof — not just a demo — that NyayaSetu gives legally grounded answers 82%82\% 82% of the time. That number gave the judges something real to evaluate.
🏁 Final Reflection We came into this hackathon with one question:
"Can we build something in 4 hours that actually helps someone in a village understand their legal rights?"
We leave with one answer: NyayaSetu=Databricks Lakehouse⏟scalable data+Multilingual RAG⏟vernacular AI+Open Legal Data⏟IPC + RTI + MGNREGA + DV Act\text{NyayaSetu} = \underbrace{\text{Databricks Lakehouse}}{\text{scalable data}} + \underbrace{\text{Multilingual RAG}}{\text{vernacular AI}} + \underbrace{\text{Open Legal Data}}_{\text{IPC + RTI + MGNREGA + DV Act}}NyayaSetu=scalable dataDatabricks Lakehouse+vernacular AIMultilingual RAG+IPC + RTI + MGNREGA + DV ActOpen Legal Data =Justice, explained. In your language. For free. In 3 seconds.= \textbf{Justice, explained. In your language. For free. In 3 seconds.}=Justice, explained. In your language. For free. In 3 seconds.
"Justice delayed is justice denied. But justice never explained — was never justice at all."
Built at Bharat Bricks Hacks 2026 | Swatantra Track | Team NyayaSetu
Built With
- databricks
- delta-lake
- faiss
- github
- google-translate
- gradio
- huggingface
- mlflow
- pyspark
- python
- sentence-transformers
- streamlit
Log in or sign up for Devpost to join the conversation.