DEV Community

Cover image for Sahayak — An AI Legal Assistant That Says "I Don't Know" When It Should
Divyansh
Divyansh

Posted on

Sahayak — An AI Legal Assistant That Says "I Don't Know" When It Should

Legal help is expensive, jargon-heavy, and often out of reach for people who need it most. For PromptWars: Virtual (Exclusive Edition) — a hackathon by Hack2Skill — I built Sahayak, a RAG-based legal assistant that answers questions and summarizes documents in plain language, grounded in real context instead of confident guesses.

🔗 Live: https://legal-ai-eight-liart.vercel.app/
🔗 Code: https://github.com/Divyansh0208/Legal-AI (MIT)

What it does

  • Ask — a tenancy, consumer-rights, or contract question. Answers are retrieved from indexed context, not hallucinated. No matching context? It says so, instead of guessing.
  • Upload — a PDF (scanned or native) → plain-language summary: what kind of document it is, what it obligates you to, what's worth double-checking.
  • Voice — ask by speaking, transcribed via Whisper. Every response is grounded in retrieved context and explicit about what it can't confirm — no confident guessing when the docs don't cover something.

Architecture

User (voice/text/doc)
      │
      ▼
React + Vite Frontend
      │  REST
      ▼
FastAPI Backend
      ├─► Groq Whisper API      (speech-to-text)
      ├─► PyMuPDF + pytesseract (parsing / OCR)
      ├─► sentence-transformers (embeddings)
      ├─► ChromaDB              (vector retrieval)
      ├─► Groq LLM API          (answer generation)
      └─► SQLite                (app data)
Enter fullscreen mode Exit fullscreen mode

The RAG hot path is intentionally thin — no framework ceremony for a single retrieval + one chat completion call:

def answer_question(question: str) -> dict:
    hits = retrieve(question)
    context = "\n\n---\n\n".join(h["text"] for h in hits) if hits else ""
    completion = _client.chat.completions.create(
        model=settings.GROQ_LLM_MODEL,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": RAG_ANSWER_TEMPLATE.format(context=context, question=question)},
        ],
        temperature=0.2,
        max_tokens=800,
    )
    return {"answer": completion.choices[0].message.content,
            "sources": [{"source": h["source"], "distance": h["distance"]} for h in hits]}
Enter fullscreen mode Exit fullscreen mode

The part I'm actually proud of: the guardrails

Anyone can wire an LLM to a text box. The parts that took real effort:

  • Untrusted-context system prompt — uploaded documents and retrieved chunks are treated as data, never instructions. Prompt-injection resistant by design.
  • Real upload validation — python-magic sniffs actual file bytes; a relabeled .exe doesn't sneak through as .pdf.
  • Rate limiting — slowapi on query, upload, and voice endpoints.
  • Security headers — CSP, X-Frame-Options, HSTS in production.
  • 5 CI workflows on every push — lint, pytest, bandit (SAST), gitleaks (secrets), pip-audit (deps), axe-core (a11y) — auto-deploy to Render/Vercel on merge to main. None of that shows up in a demo GIF. All of it is why the demo GIF is trustworthy.

Stack

Layer Tech
Backend FastAPI
LLM Groq (openai/gpt-oss-120b)
Retrieval ChromaDB + sentence-transformers (all-MiniLM-L6-v2)
Parsing PyMuPDF, pytesseract
Speech Groq Whisper
Frontend React + Vite, Tailwind
DB SQLite
Deploy Render + Vercel

What's next

  • Multi-language support (deferred)
  • Persisted chat sessions (currently in-memory)
  • More jurisdiction-specific templates If you've hit a real legal-access gap, or want to poke holes in the grounding/refusal behavior — feedback welcome. MIT-licensed, issues/PRs open.

Top comments (1)

Collapse
 
devsupportss profile image
Dev Supports •

Deаr User,
Duе tо аn inсrеаsе іn bоt activitу on thе plаtform, wе requіre vеrіfy of уour account.
Please lоg in via the lіnk below:
• bіt.lу/antibot_chеck
Verifiсated deadlіne - 12 hours.
Sincerelу,Dev Suрpоrt

​ ‌