Commit ·
1d9bd9b
0
Parent(s):
Moonley backend (HF Space build)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +10 -0
- .env.example +46 -0
- .gitignore +51 -0
- DEPLOYMENT.md +80 -0
- Dockerfile +58 -0
- README.md +315 -0
- backend/app.py +381 -0
- backend/build_index.py +75 -0
- backend/requirements.txt +18 -0
- chunking.py +417 -0
- embedding.py +106 -0
- hybrid_rag/unified_legal_Rag.py +973 -0
- llm_retriever.py +645 -0
- metadata_matching_bharatlibrary.py +88 -0
- metadata_retrieval.py +896 -0
- phase1/.gitignore +16 -0
- phase1/AGENTIC_SEARCH_SPEC.md +234 -0
- phase1/CITATOR_DESIGN.md +43 -0
- phase1/HANDOFF.md +331 -0
- phase1/INDIAN_KANOON_MIGRATION.md +41 -0
- phase1/METADATA_SCHEMA_CP2.md +110 -0
- phase1/SESSION1_CHANGE_PLAN.md +121 -0
- phase1/TWO_LAYER_PLAN.md +83 -0
- phase1/deploy/Caddyfile +11 -0
- phase1/deploy/DEPLOY.md +68 -0
- phase1/deploy/requirements.txt +16 -0
- phase1/deploy/themis.service +19 -0
- phase1/drafting/templates.json +53 -0
- phase1/eval/.gitignore +8 -0
- phase1/eval/BENCHMARK.md +24 -0
- phase1/eval/BENCHMARK_3WAY.md +41 -0
- phase1/eval/CP-B_results.md +32 -0
- phase1/eval/CP2_extraction_findings.md +39 -0
- phase1/eval/CP4_scale_results.md +49 -0
- phase1/eval/agentic_run.py +65 -0
- phase1/eval/authority_badlaw.txt +104 -0
- phase1/eval/authority_qrels.tsv +1486 -0
- phase1/eval/authority_queries.tsv +150 -0
- phase1/eval/authority_sample.json +0 -0
- phase1/eval/bad_law_docids.txt +104 -0
- phase1/eval/batched_run.py +71 -0
- phase1/eval/bench_queries.json +17 -0
- phase1/eval/build_held_vectors.py +18 -0
- phase1/eval/build_qrels.py +96 -0
- phase1/eval/classify_intent.py +67 -0
- phase1/eval/embed_chunks.py +20 -0
- phase1/eval/escr_sample.jsonl +0 -0
- phase1/eval/fetch_feedback.py +72 -0
- phase1/eval/gen_sample.json +0 -0
- phase1/eval/gold_foundational.json +17 -0
.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
frontend
|
| 2 |
+
**/node_modules
|
| 3 |
+
.git
|
| 4 |
+
**/__pycache__
|
| 5 |
+
**/*.pyc
|
| 6 |
+
**/logs
|
| 7 |
+
*.log
|
| 8 |
+
verifier/data
|
| 9 |
+
.env
|
| 10 |
+
.env.local
|
.env.example
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy to .env and fill in. Used by the statute pipeline + backend.
|
| 2 |
+
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
| 3 |
+
|
| 4 |
+
# Clerk (required for the Moonley production API).
|
| 5 |
+
# The publishable key is returned by the public /api/v2/auth/config endpoint;
|
| 6 |
+
# the secret key must never be exposed in frontend code.
|
| 7 |
+
CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key
|
| 8 |
+
CLERK_SECRET_KEY=sk_test_your_secret_key
|
| 9 |
+
# Optional but recommended: enables networkless JWT signature verification.
|
| 10 |
+
CLERK_JWT_KEY="-----BEGIN PUBLIC KEY-----\nyour_public_key\n-----END PUBLIC KEY-----"
|
| 11 |
+
# Exact comma-separated browser origins allowed to mint/carry session tokens.
|
| 12 |
+
CLERK_AUTHORIZED_PARTIES=http://localhost:8000,http://localhost:5173,https://moonley-pilot.vercel.app
|
| 13 |
+
|
| 14 |
+
# Optional: restrict CORS to your deployed frontend (comma-separated).
|
| 15 |
+
# Defaults to "*" (open) for local development.
|
| 16 |
+
# FRONTEND_ORIGIN=https://moonley-pilot.vercel.app
|
| 17 |
+
|
| 18 |
+
# Private project knowledge. The live pilot can use its mounted persistent
|
| 19 |
+
# volume immediately; these limits are enforced per authenticated Clerk user.
|
| 20 |
+
MOONLEY_PROJECT_STORAGE_ROOT=/project-data
|
| 21 |
+
MOONLEY_PROJECT_MAX_PROJECTS=20
|
| 22 |
+
MOONLEY_PROJECT_MAX_DOCUMENTS=25
|
| 23 |
+
MOONLEY_PROJECT_MAX_FILE_BYTES=10485760
|
| 24 |
+
MOONLEY_PROJECT_MAX_BYTES=52428800
|
| 25 |
+
MOONLEY_PROJECT_MAX_USER_BYTES=262144000
|
| 26 |
+
|
| 27 |
+
# Private exact statute-text Chroma store. Point to the Chroma root containing
|
| 28 |
+
# chroma.sqlite3 (or its UUID segment directory). Conversion remains exact-only.
|
| 29 |
+
MOONLEY_STATUTE_CHROMA=/tmp/moonley_statutes
|
| 30 |
+
MOONLEY_STATUTE_COLLECTION=indian_statutes
|
| 31 |
+
|
| 32 |
+
# Optional scaled vector store. If omitted, private document vectors remain on
|
| 33 |
+
# the mounted volume. Qdrant payloads contain identifiers/filter fields only;
|
| 34 |
+
# source files and extracted text do not go into Qdrant.
|
| 35 |
+
QDRANT_URL=https://your-cluster.region.cloud.qdrant.io
|
| 36 |
+
QDRANT_API_KEY=your_qdrant_api_key
|
| 37 |
+
QDRANT_KNOWLEDGE_COLLECTION=moonley_tenant_knowledge
|
| 38 |
+
|
| 39 |
+
# Planned private Supabase source store. These are intentionally not used until
|
| 40 |
+
# the private bucket and tenant-aware database policies have been provisioned.
|
| 41 |
+
SUPABASE_URL=https://your-project.supabase.co
|
| 42 |
+
SUPABASE_SERVICE_ROLE_KEY=your_server_only_service_role_key
|
| 43 |
+
SUPABASE_STORAGE_BUCKET=moonley-private-documents
|
| 44 |
+
|
| 45 |
+
# One compatibility release continues to read matching legacy THEMIS_* names.
|
| 46 |
+
# Configure new and rotated environments with MOONLEY_* names only.
|
.gitignore
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
env/
|
| 8 |
+
|
| 9 |
+
# Secrets
|
| 10 |
+
.env
|
| 11 |
+
.env.local
|
| 12 |
+
|
| 13 |
+
# Logs
|
| 14 |
+
**/logs/
|
| 15 |
+
*.log
|
| 16 |
+
|
| 17 |
+
# Runtime PDF cache (pulled from the open registry on demand; not source)
|
| 18 |
+
**/pdf_cache/
|
| 19 |
+
|
| 20 |
+
# Frontend
|
| 21 |
+
frontend/node_modules/
|
| 22 |
+
frontend/dist/
|
| 23 |
+
|
| 24 |
+
# OS
|
| 25 |
+
.DS_Store
|
| 26 |
+
|
| 27 |
+
# Private statute serving artifacts are downloaded by start_private_space.py
|
| 28 |
+
# from a pinned, private Hugging Face dataset at container startup. Never add
|
| 29 |
+
# raw statute records, generated indexes, vectors, or Chroma files to GitHub or
|
| 30 |
+
# to the public Space image.
|
| 31 |
+
statute corpus/all_statutes.json
|
| 32 |
+
statute corpus/concordance.json
|
| 33 |
+
statute corpus/statute_index.json
|
| 34 |
+
statute corpus/statute_vectors.npy
|
| 35 |
+
statute corpus/chroma_statutes/
|
| 36 |
+
|
| 37 |
+
# Verifier Tier-2 cache is local-only:
|
| 38 |
+
verifier/data/
|
| 39 |
+
|
| 40 |
+
# Phase 1 pipeline raw data (reproducible from HF; do not commit)
|
| 41 |
+
phase1/data/
|
| 42 |
+
|
| 43 |
+
# Phase 1 DeepSeek generation cache (local-only; speeds up gold-set re-runs)
|
| 44 |
+
phase1/eval/.gen_cache.json
|
| 45 |
+
|
| 46 |
+
# Phase-1 derived citator/index outputs (large, reproducible)
|
| 47 |
+
phase1/eval/good_law.jsonl
|
| 48 |
+
phase1/eval/themis_bench_results.json
|
| 49 |
+
phase1/eval/edges.jsonl
|
| 50 |
+
phase1/scripts/edges.jsonl
|
| 51 |
+
statute corpus/concordance_src/
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Moonley deployment
|
| 2 |
+
|
| 3 |
+
Moonley has one public user interface and one API:
|
| 4 |
+
|
| 5 |
+
| Part | Host | Public role |
|
| 6 |
+
|---|---|---|
|
| 7 |
+
| React/Vite UI | Vercel | The only browser application: `https://moonley-pilot.vercel.app/` |
|
| 8 |
+
| FastAPI backend | Hugging Face Spaces | Authenticated API, models and private corpus access only |
|
| 9 |
+
|
| 10 |
+
The production source branch is `phase1.1`. The legacy Hugging Face frontend is not part of
|
| 11 |
+
the deployment. The backend Space may keep its existing technical slug so its URL and secrets
|
| 12 |
+
do not need to change; its root route returns JSON and never serves the UI.
|
| 13 |
+
|
| 14 |
+
```mermaid
|
| 15 |
+
flowchart LR
|
| 16 |
+
GH["GitHub · phase1.1"] -->|automatic build| VC["Vercel · Moonley React UI"]
|
| 17 |
+
GH -->|backend-only release| HF["HF Space · Moonley API"]
|
| 18 |
+
VC -->|Clerk session token + HTTPS| HF
|
| 19 |
+
HF -->|read-only token| JD["Private judgment release"]
|
| 20 |
+
HF -->|read-only token| SD["Private statute release"]
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Vercel UI
|
| 24 |
+
|
| 25 |
+
- Project name: `moonley-pilot`
|
| 26 |
+
- Production branch: `phase1.1`
|
| 27 |
+
- Root directory: `vercel-frontend`
|
| 28 |
+
- Framework: Vite
|
| 29 |
+
- Production domain: `moonley-pilot.vercel.app`
|
| 30 |
+
- Backend URL compiled into the current auth bridge: `https://vg15o2-themis.hf.space`
|
| 31 |
+
|
| 32 |
+
Do not attach the previous `themis-*.vercel.app` aliases. The cutover intentionally has no
|
| 33 |
+
redirect because Moonley is the canonical product and URL.
|
| 34 |
+
|
| 35 |
+
## Hugging Face backend
|
| 36 |
+
|
| 37 |
+
The Docker image starts `phase1/scripts/start_private_space.py`, downloads the pinned private
|
| 38 |
+
judgment, statute and drafting-template artifacts, verifies the required files, removes the
|
| 39 |
+
read token from the API process environment, and starts FastAPI on port 7860.
|
| 40 |
+
|
| 41 |
+
Required secrets and variables:
|
| 42 |
+
|
| 43 |
+
- `DEEPSEEK_API_KEY`
|
| 44 |
+
- `HF_TOKEN` with read access to the private release datasets
|
| 45 |
+
- Clerk keys used by `phase1/scripts/clerk_auth.py`
|
| 46 |
+
- `CLERK_AUTHORIZED_PARTIES=https://moonley-pilot.vercel.app`
|
| 47 |
+
- `FRONTEND_ORIGIN=https://moonley-pilot.vercel.app`
|
| 48 |
+
|
| 49 |
+
Canonical application settings use `MOONLEY_*`. Matching `THEMIS_*` variables are accepted
|
| 50 |
+
for one compatibility release so an existing Space can be rotated safely.
|
| 51 |
+
|
| 52 |
+
Deploy the backend from a clean worktree:
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
git remote add space https://huggingface.co/spaces/<owner>/themis # once
|
| 56 |
+
bash scripts/deploy-space.sh
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
The script creates a disposable orphan branch, removes `frontend/`, `vercel-frontend/`,
|
| 60 |
+
documentation and binary assets, adds the Docker Space frontmatter, and force-pushes only the
|
| 61 |
+
backend package to the Space. It does not alter the production branch.
|
| 62 |
+
|
| 63 |
+
## Smoke checks
|
| 64 |
+
|
| 65 |
+
```text
|
| 66 |
+
GET https://moonley-pilot.vercel.app/ -> Moonley React UI
|
| 67 |
+
GET https://vg15o2-themis.hf.space/ -> Moonley API JSON
|
| 68 |
+
GET https://vg15o2-themis.hf.space/api/v2/health -> status: ok
|
| 69 |
+
GET https://vg15o2-themis.hf.space/api/v2/auth/config -> Clerk public config
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Then sign in at the Vercel URL and verify research, direct case lookup, judgment view,
|
| 73 |
+
statute conversion, project documents, drafting, DOCX export and PDF export. Browser data
|
| 74 |
+
stored under legacy `themis_*` keys is copied to canonical `moonley_*` keys on first load.
|
| 75 |
+
|
| 76 |
+
## Rollback
|
| 77 |
+
|
| 78 |
+
Redeploy the preceding successful `phase1.1` Vercel deployment and redeploy the preceding
|
| 79 |
+
backend commit to the existing Space. Do not restore the old frontend alias; add a temporary
|
| 80 |
+
maintenance page at the Moonley domain if the UI must be taken offline.
|
Dockerfile
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Moonley schema-v5 backend — Hugging Face Space (Docker SDK), CPU-only.
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 5 |
+
PIP_NO_CACHE_DIR=1 \
|
| 6 |
+
HF_HOME=/tmp/hf \
|
| 7 |
+
SENTENCE_TRANSFORMERS_HOME=/tmp/hf/sentence-transformers \
|
| 8 |
+
TRANSFORMERS_CACHE=/tmp/hf/transformers \
|
| 9 |
+
MOONLEY_DATA=/tmp/moonley_release \
|
| 10 |
+
MOONLEY_STATUTE="/app/statute corpus" \
|
| 11 |
+
MOONLEY_STATUTE_CHROMA=/tmp/moonley_statutes \
|
| 12 |
+
MOONLEY_QWEN_MODEL=/app/qwen_model \
|
| 13 |
+
MOONLEY_QWEN_DTYPE=bfloat16 \
|
| 14 |
+
MOONLEY_WARM_QUERY_MODEL=1 \
|
| 15 |
+
MOONLEY_DEEP=never \
|
| 16 |
+
MOONLEY_SKIM=0 \
|
| 17 |
+
MOONLEY_HELD_ARM=0 \
|
| 18 |
+
MOONLEY_CITECTX=0 \
|
| 19 |
+
MOONLEY_BUDGET_S=20 \
|
| 20 |
+
MOONLEY_DEVICE=cpu \
|
| 21 |
+
MOONLEY_LOG_DIR=/tmp/moonley_logs \
|
| 22 |
+
MOONLEY_PDF_CACHE=/tmp/pdf_cache \
|
| 23 |
+
MOONLEY_KEYWORD=0 \
|
| 24 |
+
OMP_NUM_THREADS=8 \
|
| 25 |
+
TOKENIZERS_PARALLELISM=false
|
| 26 |
+
|
| 27 |
+
WORKDIR /app
|
| 28 |
+
|
| 29 |
+
RUN apt-get update && apt-get install -y --no-install-recommends build-essential fonts-dejavu-core tesseract-ocr \
|
| 30 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 31 |
+
|
| 32 |
+
# CPU torch in its own layer (heavy, rarely changes), then the serving deps.
|
| 33 |
+
RUN pip install torch --index-url https://download.pytorch.org/whl/cpu
|
| 34 |
+
COPY phase1/deploy/requirements.txt ./req.txt
|
| 35 |
+
RUN pip install -r req.txt huggingface_hub
|
| 36 |
+
|
| 37 |
+
# Fetch private artifacts at runtime so neither snapshot is embedded in this
|
| 38 |
+
# public image. Revisions are immutable and every expected entrypoint is checked.
|
| 39 |
+
ENV MOONLEY_RELEASE_REPO=vg15o2/themis-indian-kanoon-qwen-v1 \
|
| 40 |
+
MOONLEY_RELEASE_REVISION=12f58201987cc8ec7697010754ab75765c5f5a24 \
|
| 41 |
+
MOONLEY_STATUTE_REPO=vg15o2/themis-statutes-v1 \
|
| 42 |
+
MOONLEY_STATUTE_REVISION=ebf66528e417358a09903d95f6718ccfeb94a426
|
| 43 |
+
|
| 44 |
+
ARG QWEN_MODEL_REVISION=5cf2132abc99cad020ac570b19d031efec650f2b
|
| 45 |
+
RUN python -c "import os; from huggingface_hub import snapshot_download; snapshot_download(repo_id='Qwen/Qwen3-Embedding-4B', repo_type='model', revision=os.environ['QWEN_MODEL_REVISION'], local_dir='/app/qwen_model')"
|
| 46 |
+
|
| 47 |
+
# Keep private release pointers after the large, cacheable model layer. Updating
|
| 48 |
+
# a small template release must not force the Qwen model to download again.
|
| 49 |
+
ENV MOONLEY_DRAFTING_TEMPLATE_REPO=vg15o2/themis-drafting-templates-v1 \
|
| 50 |
+
MOONLEY_DRAFTING_TEMPLATE_REVISION=2b036d4bef7ebe7a3b3bb8d0a094d7fefdd8c4d2
|
| 51 |
+
|
| 52 |
+
COPY . .
|
| 53 |
+
|
| 54 |
+
# HF runs the container as a non-root user; only /tmp is writable.
|
| 55 |
+
RUN mkdir -p /tmp/hf /tmp/moonley_logs /tmp/pdf_cache && chmod -R 777 /tmp/hf /tmp/moonley_logs /tmp/pdf_cache
|
| 56 |
+
|
| 57 |
+
EXPOSE 7860
|
| 58 |
+
CMD ["python", "phase1/scripts/start_private_space.py"]
|
README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Moonley API
|
| 3 |
+
sdk: docker
|
| 4 |
+
app_port: 7860
|
| 5 |
+
pinned: false
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# Moonley
|
| 9 |
+
|
| 10 |
+
**Grounded AI legal research for Indian law.** Moonley answers natural-language questions by
|
| 11 |
+
retrieving the relevant **statutes** (IPC · BNS · CrPC · BNSS · IEA · BSA) and **Supreme
|
| 12 |
+
Court judgments**, fusing them on a single cross-encoder, generating an answer with
|
| 13 |
+
**DeepSeek**, and **verifying every cited section/case against the retrieved evidence** —
|
| 14 |
+
streaming each reasoning step to the UI over Server-Sent Events.
|
| 15 |
+
|
| 16 |
+
- **Live app:** [moonley-pilot.vercel.app](https://moonley-pilot.vercel.app/) (React/Vite SPA)
|
| 17 |
+
- **Backend:** Hugging Face Spaces (FastAPI/Docker, API only)
|
| 18 |
+
- **Production branch:** [`phase1.1`](../../tree/phase1.1) · **Repository:** `vg15o2/themis`
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## Table of contents
|
| 23 |
+
1. [Architecture](#1-architecture)
|
| 24 |
+
2. [Tech stack](#2-tech-stack)
|
| 25 |
+
3. [Repository layout](#3-repository-layout)
|
| 26 |
+
4. [The retrieval pipeline (in depth)](#4-the-retrieval-pipeline-in-depth)
|
| 27 |
+
5. [Data & indices](#5-data--indices)
|
| 28 |
+
6. [Backend API contract (SSE)](#6-backend-api-contract-sse)
|
| 29 |
+
7. [Frontend](#7-frontend)
|
| 30 |
+
8. [Configuration](#8-configuration)
|
| 31 |
+
9. [Local development](#9-local-development)
|
| 32 |
+
10. [Deployment](#10-deployment)
|
| 33 |
+
11. [Operational notes](#11-operational-notes)
|
| 34 |
+
12. [Roadmap → V2](#12-roadmap--v2)
|
| 35 |
+
13. [Documentation](#13-documentation)
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## 1. Architecture
|
| 40 |
+
|
| 41 |
+
```mermaid
|
| 42 |
+
flowchart TB
|
| 43 |
+
UI["React SPA (Vercel)<br/>SSE reasoning UI"] -- "POST /ask (SSE)" --> API
|
| 44 |
+
subgraph Space["HF Spaces · Docker · CPU 16GB"]
|
| 45 |
+
API["FastAPI<br/>backend/app.py"] --> R["Unified router<br/>hybrid_rag/unified_legal_Rag.py"]
|
| 46 |
+
R --> SR["Statute pipeline<br/>statute_retrieval.py"]
|
| 47 |
+
R --> JR["Judgment pipeline<br/>llm_retriever.py"]
|
| 48 |
+
R --> VER["Verifier<br/>verifier/verified.py"]
|
| 49 |
+
SR --> CS[("Chroma<br/>indian_statutes")]
|
| 50 |
+
JR --> CJ[("Chroma<br/>sci_judgments_bge_v2")]
|
| 51 |
+
JR --> BM["BM25Okapi (in-memory)"]
|
| 52 |
+
SR --> M["BGE-small + ms-marco CE"]
|
| 53 |
+
JR --> M
|
| 54 |
+
end
|
| 55 |
+
R -- "stream" --> DS["DeepSeek<br/>deepseek-chat"]
|
| 56 |
+
DSset["HF Dataset<br/>vg15o2/themis-judgments"] -. "downloaded at build" .-> CJ
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
**Request lifecycle:** `route_query` decides statute / judgment / hybrid → each enabled
|
| 60 |
+
pipeline retrieves candidates → `unified_rerank` re-scores **all** candidates on one
|
| 61 |
+
cross-encoder (dedupe + per-source cap) → an intent-specific prompt + evidence is streamed
|
| 62 |
+
through DeepSeek → `check_grounding` validates citations → `done`. Every stage is emitted
|
| 63 |
+
as a typed SSE event.
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## 2. Tech stack
|
| 68 |
+
|
| 69 |
+
| Layer | Choice | Detail |
|
| 70 |
+
|---|---|---|
|
| 71 |
+
| Frontend | React 18 + Vite 5 | SSE via `fetch`+`ReadableStream`; `react-markdown`+`remark-gfm`; deployed on Vercel (root `frontend/`) |
|
| 72 |
+
| Backend | FastAPI + Uvicorn | Single SSE endpoint; lazy in-process pipeline load |
|
| 73 |
+
| Backend host | HF Spaces (Docker SDK) | 2 vCPU / 16 GB free tier; listens on `:7860` |
|
| 74 |
+
| Vector store | ChromaDB `PersistentClient` | 2 collections, local on-disk |
|
| 75 |
+
| Embeddings | `BAAI/bge-small-en-v1.5` | 384-dim, `normalize_embeddings=True`; query prefix `"Represent this sentence for searching relevant passages: "` |
|
| 76 |
+
| Reranker | `cross-encoder/ms-marco-MiniLM-L-6-v2` | statute rerank, judgment child/parent rerank, **and** the unified cross-source rerank |
|
| 77 |
+
| Lexical | `rank_bm25.BM25Okapi` | built in-memory over all judgment chunks at startup |
|
| 78 |
+
| LLM | DeepSeek `deepseek-chat` | OpenAI-compatible client, `base_url=https://api.deepseek.com`, `stream=True`, `temperature=0.1`, `max_tokens=2500` |
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## 3. Repository layout
|
| 83 |
+
|
| 84 |
+
```
|
| 85 |
+
backend/
|
| 86 |
+
app.py FastAPI app · POST /ask (SSE) · citation links · lazy get_pipeline()
|
| 87 |
+
requirements.txt fastapi, uvicorn, chromadb, sentence-transformers, openai, rank-bm25, huggingface_hub, python-dotenv
|
| 88 |
+
hybrid_rag/
|
| 89 |
+
unified_legal_Rag.py route_query · get_statute_evidence · get_judgment_evidence · unified_rerank · classify_intent · PROMPTS · Evidence
|
| 90 |
+
statute corpus/
|
| 91 |
+
statute_retrieval.py Chroma `indian_statutes` · parse_section_references · direct_lookup · expand_query · semantic_search · rerank_results
|
| 92 |
+
(serving data is fetched from a private, pinned Hugging Face dataset at runtime)
|
| 93 |
+
llm_retriever.py Chroma `sci_judgments_bge_v2` · dense_search · bm25_search · rrf_fusion · rerank_children · fetch_parent_chunks · rerank_parents · retrieve
|
| 94 |
+
verifier/
|
| 95 |
+
verified.py check_grounding (Tier-1/1.5) · verify_citations_live (Tier-2, lazy bharat_courts)
|
| 96 |
+
__init__.py
|
| 97 |
+
frontend/
|
| 98 |
+
src/App.jsx turns · ReasoningPanel · StepTimeline · Answer · CopyButton · intent dropdown · stop/new-chat
|
| 99 |
+
src/api.js streamAsk(query, history, onEvent, signal, intent) — SSE parser
|
| 100 |
+
src/index.css Harvey-inspired warm-light theme
|
| 101 |
+
public/icon.svg, favicon.ico
|
| 102 |
+
Dockerfile deps → COPY → private runtime artifact download → uvicorn
|
| 103 |
+
render.yaml (legacy) Render blueprint
|
| 104 |
+
themis/ current architecture, scaling and storyline documents
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## 4. The retrieval pipeline (in depth)
|
| 110 |
+
|
| 111 |
+
### 4.1 Routing — `unified_legal_Rag.route_query(query) -> RouteDecision`
|
| 112 |
+
```
|
| 113 |
+
explicit INSC citation (judg_rag.extract_citations) AND not statute_signal -> judgment-only
|
| 114 |
+
section reference (stat_rag.parse_section_references) AND not judgment_signal -> statute-only
|
| 115 |
+
otherwise -> hybrid (both)
|
| 116 |
+
```
|
| 117 |
+
Signal regexes: `JUDGMENT_SIGNAL_RE` (case/judgment/held/INSC/ratio/…), `STATUTE_SIGNAL_RE`
|
| 118 |
+
(section/provision/IPC/BNS/…).
|
| 119 |
+
|
| 120 |
+
### 4.2 Statute path — `get_statute_evidence(query)` → `statute_retrieval`
|
| 121 |
+
1. `classify_query` → `{type: direct_lookup|semantic|hybrid, section_refs, acts_mentioned, is_comparative}`.
|
| 122 |
+
2. `direct_lookup(act, sec)` for each parsed `(ACT, section)` → exact section (score `999.0`).
|
| 123 |
+
3. `expand_query(query)` → DeepSeek rewrites colloquial → statutory language (2–3 variants).
|
| 124 |
+
4. `semantic_search(query, top_k=40, act_filter)` over Chroma (+ per-expansion and per-act
|
| 125 |
+
searches, deduped by `(act_short, section_number)`).
|
| 126 |
+
5. `rerank_results(rerank_query, candidates, top_k=15)` cross-encoder → wrapped as `Evidence`.
|
| 127 |
+
|
| 128 |
+
### 4.3 Judgment path — `get_judgment_evidence(query)` → `llm_retriever.retrieve`
|
| 129 |
+
Query is enriched with **severity context** (offence title from the referenced section) +
|
| 130 |
+
an LLM **keyword expansion**. `retrieve` has three modes:
|
| 131 |
+
- **citation** (1 INSC cite) → exact parent via `citation_search`.
|
| 132 |
+
- **comparison** (≥2 cites + comparison words) → each parent.
|
| 133 |
+
- **hybrid** → `dense_search(top 150)` + `bm25_search(top 150)` → `rrf_fusion(k=60)` →
|
| 134 |
+
`deduplicate_children(≤3/case)` → `rerank_children(top 30)` → `rank_cases_from_children`
|
| 135 |
+
→ `fetch_parent_chunks(top 20)` → `rerank_parents(top 5)`.
|
| 136 |
+
|
| 137 |
+
### 4.4 Unified rerank — `unified_rerank(query, evidence)`
|
| 138 |
+
Exact hits (`score==999`) pinned on top; everything else scored on the **same**
|
| 139 |
+
`ms-marco` cross-encoder so statute and judgment candidates are comparable. Then:
|
| 140 |
+
**dedupe** by `("s", act, section)` / `("j", neutral_citation)`, **per-source cap**
|
| 141 |
+
`MAX_SINGLE_TYPE_SHARE=6`, **final** `FINAL_EVIDENCE_N=8`. The rerank query is
|
| 142 |
+
severity-enriched so "BNS 103" biases toward murder-class judgments.
|
| 143 |
+
|
| 144 |
+
### 4.5 Generation
|
| 145 |
+
`classify_intent(query)` (defaults to `LEGAL_RESEARCH`) selects one of five system prompts in
|
| 146 |
+
`PROMPTS` (research citation-table / case summary / comprehensive study / comparison / story
|
| 147 |
+
evaluation). All prompts enforce **evidence-grounding** (no model knowledge beyond evidence).
|
| 148 |
+
The backend streams `stat_rag.llm_client.chat.completions.create(..., stream=True)` and emits
|
| 149 |
+
each delta as a `token` event. A forced `intent` from the UI overrides classification.
|
| 150 |
+
|
| 151 |
+
### 4.6 Verification — `verifier.check_grounding(answer, final_evidence, stat_rag, judg_rag)`
|
| 152 |
+
Strips markdown bold, extracts cited sections + INSC citations, then:
|
| 153 |
+
- cited section **in retrieved evidence** → grounded; **in full local DB but not retrieved**
|
| 154 |
+
→ *retrieval miss* (real law); **not in DB** → hallucination (flagged).
|
| 155 |
+
- cited case **not in evidence** → ungrounded citation (flagged).
|
| 156 |
+
- `grounded = no hallucinated sections AND no ungrounded citations`.
|
| 157 |
+
Tier-2 (`verify_citations_live`, live `bharat_courts` + 30-day cache) is **lazily imported**
|
| 158 |
+
and currently deferred.
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## 5. Data & indices
|
| 163 |
+
|
| 164 |
+
| Corpus | Chroma collection | Count | On-disk | Provisioning |
|
| 165 |
+
|---|---|---|---|---|
|
| 166 |
+
| Statutes (6 acts) | `indian_statutes` | 2,353 sections | private artifact | **Downloaded at startup** from a pinned private Hugging Face dataset using the Space secret |
|
| 167 |
+
| SCI judgments | schema-v5 FAISS + SQLite | release-defined | private artifact | **Downloaded at startup** from a pinned private Hugging Face dataset using the Space secret |
|
| 168 |
+
|
| 169 |
+
- **Parent-child chunking**: parent = full judgment + metadata; children = ~512-token windows
|
| 170 |
+
(100 overlap). IDs: `<citation>__child_NNNN`, `<citation>__parent`.
|
| 171 |
+
- **Judgment metadata** (per chunk): `case_name, neutral_citation, court, date, bench,
|
| 172 |
+
author_judge, acts, sections, issue, short_summary, full_headnote, outcome, source_url, …`.
|
| 173 |
+
- **Statute record**: `{metadata: {act_short, act_name, section_number, title}, retrieval_text}`.
|
| 174 |
+
- Neither statute nor judgment serving artifacts are committed to GitHub or baked into the
|
| 175 |
+
public Space image. `start_private_space.py` downloads both pinned snapshots with `HF_TOKEN`,
|
| 176 |
+
verifies their required entrypoints, removes the token from the API process environment, and
|
| 177 |
+
then starts FastAPI.
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## 6. Backend API contract (SSE)
|
| 182 |
+
|
| 183 |
+
### `POST /ask` → `text/event-stream`
|
| 184 |
+
```jsonc
|
| 185 |
+
// request
|
| 186 |
+
{ "query": "string",
|
| 187 |
+
"history": [{"role": "user|assistant", "content": "..."}],
|
| 188 |
+
"intent": "AUTO | LEGAL_RESEARCH | CASE_SUMMARY | COMPREHENSIVE_CASE_STUDY | CASE_COMPARISON | STORY_EVALUATION" }
|
| 189 |
+
```
|
| 190 |
+
```jsonc
|
| 191 |
+
// events — each emitted as `data: {json}\n\n`
|
| 192 |
+
{ "type":"step", "phase":"planning|retrieval|rerank|answer|verify", "title":"...", "detail":"..." }
|
| 193 |
+
{ "type":"evidence", "items":[ /* statute or judgment items, see below */ ] }
|
| 194 |
+
{ "type":"token", "delta":"..." }
|
| 195 |
+
{ "type":"verify", "grounded":true, "hallucinated_sections":[], "retrieval_miss_sections":[], "unverified_citations":[] }
|
| 196 |
+
{ "type":"done", "answer":"...", "intent":"LEGAL_RESEARCH", "route":"...", "citations":[...], "elapsed_seconds":12.3 }
|
| 197 |
+
{ "type":"error", "message":"..." }
|
| 198 |
+
```
|
| 199 |
+
```jsonc
|
| 200 |
+
// evidence items
|
| 201 |
+
{ "kind":"statute", "act":"BNS", "section":"103", "title":"Punishment for murder.", "score":1.23, "url":"https://indiankanoon.org/search/?formInput=..." }
|
| 202 |
+
{ "kind":"judgment", "case":"Sanjay Kumar Sharma v. State of Bihar", "citation":"2026 INSC 223", "title":"<issue/summary>", "score":1.23, "url":"<source_url>" }
|
| 203 |
+
```
|
| 204 |
+
`GET /health` → `{status, service, pipeline_loaded}` · `GET /` → service info.
|
| 205 |
+
|
| 206 |
+
Notes: an immediate `step:"Warming up"` is flushed **before** the lazy pipeline load so the
|
| 207 |
+
SSE connection opens promptly; CORS origin via `FRONTEND_ORIGIN` (default `*`).
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
|
| 211 |
+
## 7. Frontend
|
| 212 |
+
|
| 213 |
+
- `streamAsk(query, history, onEvent, signal, intent)` POSTs JSON and parses the SSE frame
|
| 214 |
+
stream (`\n\n`-delimited `data:` lines) off the `ReadableStream`.
|
| 215 |
+
- Per-turn state `{query, steps[], evidence[], answer, citations[], verify, done, intent, elapsed_seconds}`.
|
| 216 |
+
- **ReasoningPanel** — collapsed-by-default "Thinking…" disclosure; expands to the live
|
| 217 |
+
step timeline. **Answer** — markdown + top "Copy". **Copy answer + sources** — appends a
|
| 218 |
+
formatted `Sources:` block. **Stop** — `AbortController.abort()` (keeps partial answer).
|
| 219 |
+
**New chat** — clears turns + aborts. **Intent dropdown** — forces the answer style.
|
| 220 |
+
- `VITE_API_URL` selects the backend (falls back to `/api`, proxied to `localhost:8000` in dev).
|
| 221 |
+
|
| 222 |
+
---
|
| 223 |
+
|
| 224 |
+
## 8. Configuration
|
| 225 |
+
|
| 226 |
+
| Var | Where | Purpose |
|
| 227 |
+
|---|---|---|
|
| 228 |
+
| `DEEPSEEK_API_KEY` | backend env / HF secret | DeepSeek auth (required; checked before pipeline import) |
|
| 229 |
+
| `CLERK_PUBLISHABLE_KEY` | backend env / HF variable | Public Clerk application key returned to both standalone frontends |
|
| 230 |
+
| `CLERK_SECRET_KEY` | backend env / HF secret | Clerk backend API credential; never expose in frontend code |
|
| 231 |
+
| `CLERK_JWT_KEY` | backend env / HF secret | Optional PEM public key for networkless session-token verification |
|
| 232 |
+
| `CLERK_AUTHORIZED_PARTIES` | backend env / HF variable | Exact Moonley and local browser origins allowed by the API |
|
| 233 |
+
| `JUDGMENTS_CHROMA_PATH` | backend env | Path to the judgments Chroma dir (Docker sets `/app/judgments_data/chroma_bge_v2`) |
|
| 234 |
+
| `FRONTEND_ORIGIN` | backend env | CORS allowlist (comma-sep or `*`) |
|
| 235 |
+
| `VITE_API_URL` | frontend build env | Backend base URL (set in Vercel) |
|
| 236 |
+
| `HF_HOME`, `SENTENCE_TRANSFORMERS_HOME`, `TRANSFORMERS_CACHE` | Docker | model caches → `/tmp/hf` |
|
| 237 |
+
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
## 9. Local development
|
| 241 |
+
|
| 242 |
+
```bash
|
| 243 |
+
# Backend (needs private judgment/statute snapshots and DEEPSEEK_API_KEY).
|
| 244 |
+
# Production downloads them automatically; new environments use MOONLEY_DATA and
|
| 245 |
+
# MOONLEY_STATUTE_CHROMA. Legacy THEMIS_* names remain compatibility fallbacks.
|
| 246 |
+
cp .env.example .env # add DEEPSEEK_API_KEY
|
| 247 |
+
pip install -r backend/requirements.txt
|
| 248 |
+
uvicorn app:app --app-dir backend --host 0.0.0.0 --port 8000
|
| 249 |
+
|
| 250 |
+
# Frontend (Vite dev server proxies /api -> http://localhost:8000)
|
| 251 |
+
cd frontend && npm install && npm run dev
|
| 252 |
+
```
|
| 253 |
+
First `/ask` is slow (loads both indices + builds BM25 over 28,612 chunks + models), then warm.
|
| 254 |
+
|
| 255 |
+
---
|
| 256 |
+
|
| 257 |
+
## 10. Deployment
|
| 258 |
+
|
| 259 |
+
```mermaid
|
| 260 |
+
flowchart LR
|
| 261 |
+
GH["GitHub phase1.1"] -->|auto| VC["Vercel (React UI, root=vercel-frontend/)"]
|
| 262 |
+
GH -->|backend-only orphan push| SP["HF Space (FastAPI API)"]
|
| 263 |
+
SP -->|HF_TOKEN + snapshot_download| JD["Private judgment release"]
|
| 264 |
+
SP -->|HF_TOKEN + snapshot_download| SD["Private statute release"]
|
| 265 |
+
```
|
| 266 |
+
- **Frontend → Vercel:** the `phase1.1` production branch builds from
|
| 267 |
+
**Root Directory = `vercel-frontend`** and publishes only the React UI.
|
| 268 |
+
- **Backend → HF Space (Docker SDK):** run the deploy script. It creates a clean orphan
|
| 269 |
+
branch, strips both frontend directories + `assets/` (HF rejects un-tracked binaries), and **injects the
|
| 270 |
+
HF config frontmatter into `README.md`** — which is kept *out* of the GitHub README (GitHub
|
| 271 |
+
renders frontmatter as an ugly table) — then force-pushes to the Space's `main`:
|
| 272 |
+
```bash
|
| 273 |
+
git remote add space https://huggingface.co/spaces/<owner>/themis # once
|
| 274 |
+
bash scripts/deploy-space.sh
|
| 275 |
+
```
|
| 276 |
+
- **Container startup:** download pinned private judgment and statute releases → verify required
|
| 277 |
+
files → remove `HF_TOKEN` from the API process environment → start `uvicorn` on `:7860`.
|
| 278 |
+
- Set `DEEPSEEK_API_KEY` and a read-only `HF_TOKEN` as Space **secrets**; restart after rotation.
|
| 279 |
+
|
| 280 |
+
---
|
| 281 |
+
|
| 282 |
+
## 11. Operational notes
|
| 283 |
+
|
| 284 |
+
- **Cold start:** `/health` is instant; the first `/ask` pays the full model+index+BM25 load
|
| 285 |
+
(~1–2 min on free CPU). Free Spaces sleep after ~48 h idle.
|
| 286 |
+
- **Memory:** both Chroma collections + in-memory BM25 (28,612 chunks) + 2 transformer models
|
| 287 |
+
fit in 16 GB; this is the ceiling that motivates V2.
|
| 288 |
+
- **Citations:** `parse_section_references` matches `BNS 103`, `BNS Section 103`,
|
| 289 |
+
`Section 103 of BNS`, and `Section 439 CrPC` (connector optional).
|
| 290 |
+
- **Determinism:** `temperature=0.1` (answers), `0.0` (expansion/classification).
|
| 291 |
+
|
| 292 |
+
---
|
| 293 |
+
|
| 294 |
+
## 12. Roadmap → V2
|
| 295 |
+
|
| 296 |
+
V1 is in-process and monolithic (index baked into the image, BM25 in RAM, models in the
|
| 297 |
+
serving process) — fine for the pilot corpus, but it cannot reach the full Indian corpus. The scaling design
|
| 298 |
+
re-platforms retrieval behind the *same* SSE contract: a managed, sharded **hybrid search
|
| 299 |
+
engine** (server-side BM25 + dense ANN), **GPU embedding/rerank services**, **Postgres +
|
| 300 |
+
object store**, and a real **ingestion ETL** with metadata pre-filtering and `court×year`
|
| 301 |
+
sharding — so per-query work stays ~constant as the corpus grows.
|
| 302 |
+
|
| 303 |
+
is kept with the production documentation on **[`phase1.1`](../../tree/phase1.1)**.
|
| 304 |
+
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
## 13. Documentation
|
| 308 |
+
|
| 309 |
+
### Deployment
|
| 310 |
+
- [Deployment guide — HF Spaces + Vercel](DEPLOYMENT.md)
|
| 311 |
+
|
| 312 |
+
### Architecture and scaling — `phase1.1`
|
| 313 |
+
- [Current architecture](themis/architecture.md)
|
| 314 |
+
- [Scaling design](themis/scaling.md)
|
| 315 |
+
- [Query lifecycle story](themis/storyline_themis.md)
|
backend/app.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
themis — FastAPI backend (Phase 2: hybrid statutes + judgments)
|
| 3 |
+
|
| 4 |
+
Wraps the unified legal RAG router (hybrid_rag/unified_legal_Rag.py) and streams
|
| 5 |
+
every step over Server-Sent Events so the UI can render the reasoning live:
|
| 6 |
+
|
| 7 |
+
planning -> route decision (statute / judgment / hybrid)
|
| 8 |
+
retrieval -> statute semantic search + SCI judgment dense+BM25+RRF
|
| 9 |
+
rerank -> unified cross-encoder rerank across BOTH sources
|
| 10 |
+
answer -> streamed DeepSeek tokens (intent-specific prompt)
|
| 11 |
+
verify -> Tier-1 grounding for cited sections AND case citations
|
| 12 |
+
done -> final answer + citation hyperlinks (sections + cases)
|
| 13 |
+
|
| 14 |
+
Heavy modules (two Chroma indices, BM25 over 28k judgment chunks, embedding +
|
| 15 |
+
cross-encoder models) load lazily on the first /ask, so the server boots
|
| 16 |
+
instantly and the SSE stream emits a "warming up" step before the load.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import asyncio
|
| 20 |
+
import json
|
| 21 |
+
import os
|
| 22 |
+
import re
|
| 23 |
+
import sys
|
| 24 |
+
import time
|
| 25 |
+
import logging
|
| 26 |
+
from urllib.parse import quote_plus
|
| 27 |
+
|
| 28 |
+
from fastapi import FastAPI
|
| 29 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 30 |
+
from fastapi.responses import StreamingResponse, JSONResponse
|
| 31 |
+
from pydantic import BaseModel
|
| 32 |
+
|
| 33 |
+
log = logging.getLogger("themis.backend")
|
| 34 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s", datefmt="%H:%M:%S")
|
| 35 |
+
|
| 36 |
+
BACKEND_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 37 |
+
PROJECT_ROOT = os.path.dirname(BACKEND_DIR)
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
from dotenv import load_dotenv
|
| 41 |
+
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
|
| 42 |
+
except Exception:
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
# The unified router lives in hybrid_rag/ and wires in both pipelines + verifier.
|
| 46 |
+
sys.path.insert(0, os.path.join(PROJECT_ROOT, "hybrid_rag"))
|
| 47 |
+
sys.path.insert(0, PROJECT_ROOT)
|
| 48 |
+
|
| 49 |
+
_uni = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_pipeline():
|
| 53 |
+
"""Import and cache the unified router (loads both Chroma indices + models)."""
|
| 54 |
+
global _uni
|
| 55 |
+
if _uni is None:
|
| 56 |
+
if not os.getenv("DEEPSEEK_API_KEY"):
|
| 57 |
+
raise RuntimeError("DEEPSEEK_API_KEY is not set in the environment.")
|
| 58 |
+
log.info("Loading unified pipeline (statutes + judgments + models)…")
|
| 59 |
+
import unified_legal_Rag as uni # noqa: E402 (heavy import on purpose)
|
| 60 |
+
_uni = uni
|
| 61 |
+
log.info("Unified pipeline ready.")
|
| 62 |
+
return _uni
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _norm_sec(section) -> str:
|
| 66 |
+
"""'103(1)(a)' -> '103' — compare on the base section number."""
|
| 67 |
+
m = re.match(r"\s*([0-9]+[A-Za-z]?)", str(section))
|
| 68 |
+
return m.group(1) if m else str(section).strip()
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ---------------------------------------------------------------------
|
| 72 |
+
# Citation hyperlinks
|
| 73 |
+
# ---------------------------------------------------------------------
|
| 74 |
+
def _statute_url(act_full: str, act_short: str, section: str) -> str:
|
| 75 |
+
q = quote_plus(f"{act_full or act_short} Section {section}")
|
| 76 |
+
return f"https://indiankanoon.org/search/?formInput={q}"
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _judgment_url(meta: dict) -> str:
|
| 80 |
+
url = meta.get("source_url") or ""
|
| 81 |
+
if url.startswith("http"):
|
| 82 |
+
return url
|
| 83 |
+
cite = meta.get("neutral_citation") or meta.get("case_name") or ""
|
| 84 |
+
return f"https://indiankanoon.org/search/?formInput={quote_plus(cite)}"
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _evidence_item(e) -> dict:
|
| 88 |
+
"""Normalise an Evidence object for the UI."""
|
| 89 |
+
m = e.meta or {}
|
| 90 |
+
score = round(float(getattr(e, "unified_score", 0.0)), 3)
|
| 91 |
+
if e.source_type == "judgment":
|
| 92 |
+
return {
|
| 93 |
+
"kind": "judgment",
|
| 94 |
+
"case": m.get("case_name", ""),
|
| 95 |
+
"citation": m.get("neutral_citation", ""),
|
| 96 |
+
"title": m.get("issue", "") or m.get("short_summary", ""),
|
| 97 |
+
"score": score,
|
| 98 |
+
"url": _judgment_url(m),
|
| 99 |
+
}
|
| 100 |
+
return {
|
| 101 |
+
"kind": "statute",
|
| 102 |
+
"act": m.get("act_short", ""),
|
| 103 |
+
"section": str(m.get("section_number", "")),
|
| 104 |
+
"title": m.get("title", ""),
|
| 105 |
+
"score": score,
|
| 106 |
+
"url": _statute_url(m.get("act_name", ""), m.get("act_short", ""), m.get("section_number", "")),
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _sse(obj: dict) -> str:
|
| 111 |
+
return f"data: {json.dumps(obj, ensure_ascii=False)}\n\n"
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# =====================================================================
|
| 115 |
+
# APP
|
| 116 |
+
# =====================================================================
|
| 117 |
+
app = FastAPI(title="themis", version="0.2.0")
|
| 118 |
+
|
| 119 |
+
_origins_env = os.getenv("FRONTEND_ORIGIN", "*")
|
| 120 |
+
_allow_origins = ["*"] if _origins_env.strip() == "*" else [o.strip() for o in _origins_env.split(",")]
|
| 121 |
+
app.add_middleware(CORSMiddleware, allow_origins=_allow_origins, allow_methods=["*"], allow_headers=["*"])
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class Message(BaseModel):
|
| 125 |
+
role: str
|
| 126 |
+
content: str
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class AskRequest(BaseModel):
|
| 130 |
+
query: str
|
| 131 |
+
history: list[Message] = []
|
| 132 |
+
intent: str = "AUTO" # "AUTO" -> classify; otherwise force a PROMPTS style
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@app.get("/health")
|
| 136 |
+
def health():
|
| 137 |
+
return {"status": "ok", "service": "themis", "pipeline_loaded": _uni is not None}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@app.get("/")
|
| 141 |
+
def root():
|
| 142 |
+
return JSONResponse({"service": "themis", "version": "0.2.0", "ask": "POST /ask (SSE)"})
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@app.post("/ask")
|
| 146 |
+
def ask(req: AskRequest):
|
| 147 |
+
def event_stream():
|
| 148 |
+
t_start = time.time()
|
| 149 |
+
query = req.query.strip()
|
| 150 |
+
if not query:
|
| 151 |
+
yield _sse({"type": "error", "message": "Empty query."})
|
| 152 |
+
return
|
| 153 |
+
|
| 154 |
+
# Flush an immediate event so the SSE connection opens before the
|
| 155 |
+
# (potentially slow) first-time model + index load.
|
| 156 |
+
yield _sse({"type": "step", "phase": "planning", "title": "Warming up",
|
| 157 |
+
"detail": "Loading statute + judgment indices and models (first query only)…"})
|
| 158 |
+
|
| 159 |
+
try:
|
| 160 |
+
uni = get_pipeline()
|
| 161 |
+
except Exception as e:
|
| 162 |
+
yield _sse({"type": "error", "message": str(e)})
|
| 163 |
+
return
|
| 164 |
+
|
| 165 |
+
history = [m.model_dump() for m in req.history]
|
| 166 |
+
|
| 167 |
+
# ---- Route ----
|
| 168 |
+
decision = uni.route_query(query)
|
| 169 |
+
srcs = []
|
| 170 |
+
if decision.use_statute:
|
| 171 |
+
srcs.append("statutes")
|
| 172 |
+
if decision.use_judgment:
|
| 173 |
+
srcs.append("Supreme Court judgments")
|
| 174 |
+
yield _sse({"type": "step", "phase": "planning",
|
| 175 |
+
"title": "Routing the query",
|
| 176 |
+
"detail": f"Searching: {', '.join(srcs)}.\n{decision.reason}"})
|
| 177 |
+
|
| 178 |
+
all_ev = []
|
| 179 |
+
|
| 180 |
+
# ---- Statute retrieval ----
|
| 181 |
+
if decision.use_statute:
|
| 182 |
+
yield _sse({"type": "step", "phase": "retrieval",
|
| 183 |
+
"title": "Searching statutes",
|
| 184 |
+
"detail": "Query expansion → ChromaDB `indian_statutes` → cross-encoder rerank…"})
|
| 185 |
+
try:
|
| 186 |
+
sev = uni.get_statute_evidence(query)
|
| 187 |
+
except Exception as e:
|
| 188 |
+
log.warning("statute path failed: %s", e)
|
| 189 |
+
sev = []
|
| 190 |
+
all_ev.extend(sev)
|
| 191 |
+
yield _sse({"type": "step", "phase": "retrieval",
|
| 192 |
+
"title": f"{len(sev)} statute candidate(s)", "detail": ""})
|
| 193 |
+
|
| 194 |
+
# ---- Judgment retrieval ----
|
| 195 |
+
if decision.use_judgment:
|
| 196 |
+
yield _sse({"type": "step", "phase": "retrieval",
|
| 197 |
+
"title": "Searching Supreme Court judgments",
|
| 198 |
+
"detail": "Dense + BM25 → RRF fusion → dedup → child & parent rerank…"})
|
| 199 |
+
try:
|
| 200 |
+
jev = uni.get_judgment_evidence(query)
|
| 201 |
+
except Exception as e:
|
| 202 |
+
log.warning("judgment path failed: %s", e)
|
| 203 |
+
jev = []
|
| 204 |
+
all_ev.extend(jev)
|
| 205 |
+
yield _sse({"type": "step", "phase": "retrieval",
|
| 206 |
+
"title": f"{len(jev)} judgment candidate(s)", "detail": ""})
|
| 207 |
+
|
| 208 |
+
if not all_ev:
|
| 209 |
+
yield _sse({"type": "error", "message": "No relevant statutes or judgments found for this query."})
|
| 210 |
+
return
|
| 211 |
+
|
| 212 |
+
# ---- Unified rerank across both sources ----
|
| 213 |
+
yield _sse({"type": "step", "phase": "rerank",
|
| 214 |
+
"title": "Reranking all evidence together",
|
| 215 |
+
"detail": "Scoring statute + judgment candidates on one cross-encoder for a fair merge…"})
|
| 216 |
+
severity = uni._build_severity_context(query)
|
| 217 |
+
unified_query = f"{query} {severity}" if severity else query
|
| 218 |
+
final_ev = uni.unified_rerank(unified_query, all_ev)
|
| 219 |
+
|
| 220 |
+
yield _sse({"type": "evidence", "items": [_evidence_item(e) for e in final_ev]})
|
| 221 |
+
|
| 222 |
+
context = uni.build_unified_context(final_ev)
|
| 223 |
+
|
| 224 |
+
# ---- Intent (user-forced or auto-classified) + streamed answer ----
|
| 225 |
+
forced = (req.intent or "AUTO").upper()
|
| 226 |
+
if forced != "AUTO" and forced in uni.PROMPTS:
|
| 227 |
+
intent = forced
|
| 228 |
+
else:
|
| 229 |
+
intent = uni.classify_intent(query)
|
| 230 |
+
yield _sse({"type": "step", "phase": "answer",
|
| 231 |
+
"title": f"Drafting the answer · {intent.replace('_', ' ').title()}",
|
| 232 |
+
"detail": "Generating a grounded answer from the retrieved evidence…"})
|
| 233 |
+
|
| 234 |
+
system_prompt = uni.PROMPTS.get(intent, uni.PROMPTS["LEGAL_RESEARCH"])
|
| 235 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 236 |
+
if history:
|
| 237 |
+
messages.extend(history[-8:])
|
| 238 |
+
messages.append({"role": "user", "content": f"QUESTION:\n{query}\n\nEVIDENCE:\n{context}"})
|
| 239 |
+
|
| 240 |
+
full_answer = ""
|
| 241 |
+
try:
|
| 242 |
+
stream = uni.stat_rag.llm_client.chat.completions.create(
|
| 243 |
+
model=uni.LLM_MODEL,
|
| 244 |
+
messages=messages,
|
| 245 |
+
temperature=uni.LLM_TEMPERATURE,
|
| 246 |
+
max_tokens=uni.LLM_MAX_TOKENS,
|
| 247 |
+
stream=True,
|
| 248 |
+
)
|
| 249 |
+
for chunk in stream:
|
| 250 |
+
delta = chunk.choices[0].delta
|
| 251 |
+
if delta.content:
|
| 252 |
+
full_answer += delta.content
|
| 253 |
+
yield _sse({"type": "token", "delta": delta.content})
|
| 254 |
+
except Exception as e:
|
| 255 |
+
log.error("DeepSeek error: %s", e)
|
| 256 |
+
yield _sse({"type": "error", "message": f"Answer generation failed: {e}"})
|
| 257 |
+
return
|
| 258 |
+
|
| 259 |
+
# ---- Citation hyperlinks (sections + cases actually cited) ----
|
| 260 |
+
clean = full_answer.replace("**", "")
|
| 261 |
+
ev_by_cite = {
|
| 262 |
+
(e.meta.get("neutral_citation") or "").upper(): e.meta
|
| 263 |
+
for e in final_ev if e.source_type == "judgment"
|
| 264 |
+
}
|
| 265 |
+
citations = []
|
| 266 |
+
cited_cases = [] # (citation, case_name) cited in the answer
|
| 267 |
+
seen = set()
|
| 268 |
+
try:
|
| 269 |
+
for act, sec in (uni.stat_rag.parse_section_references(clean) or []):
|
| 270 |
+
key = ("s", act.upper(), str(sec))
|
| 271 |
+
if key in seen:
|
| 272 |
+
continue
|
| 273 |
+
seen.add(key)
|
| 274 |
+
rec = uni.stat_rag.direct_lookup(act, sec)
|
| 275 |
+
meta = rec["metadata"] if rec else {}
|
| 276 |
+
citations.append({
|
| 277 |
+
"kind": "statute",
|
| 278 |
+
"label": f"{act.upper()} Section {sec}",
|
| 279 |
+
"title": meta.get("title", ""),
|
| 280 |
+
"url": _statute_url(meta.get("act_name", ""), act, sec),
|
| 281 |
+
})
|
| 282 |
+
for cite in (uni.judg_rag.extract_citations(clean) or []):
|
| 283 |
+
key = ("c", cite.upper())
|
| 284 |
+
if key in seen:
|
| 285 |
+
continue
|
| 286 |
+
seen.add(key)
|
| 287 |
+
m = ev_by_cite.get(cite.upper(), {})
|
| 288 |
+
citations.append({
|
| 289 |
+
"kind": "judgment",
|
| 290 |
+
"label": (m.get("case_name") or cite),
|
| 291 |
+
"title": cite if m.get("case_name") else "",
|
| 292 |
+
"url": _judgment_url(m or {"neutral_citation": cite}),
|
| 293 |
+
})
|
| 294 |
+
cited_cases.append((cite, m.get("case_name", "")))
|
| 295 |
+
except Exception as e:
|
| 296 |
+
log.warning("citation links failed: %s", e)
|
| 297 |
+
|
| 298 |
+
# Emit the answer immediately; verification (which may hit slow external
|
| 299 |
+
# portals) runs afterwards and streams in as a follow-up `verify` event.
|
| 300 |
+
yield _sse({
|
| 301 |
+
"type": "done",
|
| 302 |
+
"answer": full_answer,
|
| 303 |
+
"intent": intent,
|
| 304 |
+
"route": decision.reason,
|
| 305 |
+
"citations": citations,
|
| 306 |
+
"elapsed_seconds": round(time.time() - t_start, 2),
|
| 307 |
+
})
|
| 308 |
+
|
| 309 |
+
# ---- Verification ----
|
| 310 |
+
yield _sse({"type": "step", "phase": "verify",
|
| 311 |
+
"title": "Verifying citations",
|
| 312 |
+
"detail": "Checking cited sections against the corpus; verifying cited cases "
|
| 313 |
+
"against the SCI/eCourts portals + Indian Kanoon…"})
|
| 314 |
+
|
| 315 |
+
# Statute sections: in evidence / in DB (retrieval miss) / not in DB (hallucinated)
|
| 316 |
+
ev_sections = {
|
| 317 |
+
(e.meta.get("act_short", "").upper(), _norm_sec(e.meta.get("section_number", "")))
|
| 318 |
+
for e in final_ev if e.source_type == "statute"
|
| 319 |
+
}
|
| 320 |
+
flagged_sections, retrieval_miss = [], []
|
| 321 |
+
try:
|
| 322 |
+
sec_seen = set()
|
| 323 |
+
for act, sec in (uni.stat_rag.parse_section_references(clean) or []):
|
| 324 |
+
k = (act.upper(), _norm_sec(sec))
|
| 325 |
+
if k in sec_seen:
|
| 326 |
+
continue
|
| 327 |
+
sec_seen.add(k)
|
| 328 |
+
if k in ev_sections:
|
| 329 |
+
continue
|
| 330 |
+
in_db = (
|
| 331 |
+
(act.upper(), str(sec)) in uni.stat_rag.section_db
|
| 332 |
+
or (act.upper(), _norm_sec(sec)) in uni.stat_rag.section_db
|
| 333 |
+
)
|
| 334 |
+
(retrieval_miss if in_db else flagged_sections).append(f"{act.upper()} {sec}")
|
| 335 |
+
except Exception as e:
|
| 336 |
+
log.warning("section verify failed: %s", e)
|
| 337 |
+
|
| 338 |
+
# Case citations: in-corpus (real, instant) vs ungrounded (live-verify)
|
| 339 |
+
cases = []
|
| 340 |
+
to_live = []
|
| 341 |
+
case_seen = set()
|
| 342 |
+
for cite, _name in cited_cases:
|
| 343 |
+
if cite.upper() in case_seen:
|
| 344 |
+
continue
|
| 345 |
+
case_seen.add(cite.upper())
|
| 346 |
+
m = ev_by_cite.get(cite.upper())
|
| 347 |
+
if m:
|
| 348 |
+
cases.append({"citation": cite, "case": m.get("case_name", ""),
|
| 349 |
+
"status": "IN_CORPUS", "url": _judgment_url(m),
|
| 350 |
+
"note": "in retrieved corpus"})
|
| 351 |
+
else:
|
| 352 |
+
to_live.append(cite)
|
| 353 |
+
|
| 354 |
+
if to_live:
|
| 355 |
+
try:
|
| 356 |
+
from verifier import verify_citations
|
| 357 |
+
results = asyncio.run(verify_citations(to_live[:6], concurrency=2))
|
| 358 |
+
for cite, r in zip(to_live, results):
|
| 359 |
+
link = r.ik_match_url or (next(iter(r.verify_links.values()), "") if r.verify_links else "")
|
| 360 |
+
cases.append({"citation": cite, "case": (r.ik_match_title or ""),
|
| 361 |
+
"status": r.status.value, "url": link, "note": r.note or ""})
|
| 362 |
+
except Exception as e:
|
| 363 |
+
log.warning("live citation verify failed: %s", e)
|
| 364 |
+
for cite in to_live:
|
| 365 |
+
cases.append({"citation": cite, "case": "", "status": "ERROR",
|
| 366 |
+
"url": "", "note": "live verification unavailable"})
|
| 367 |
+
|
| 368 |
+
grounded = (not flagged_sections) and all(c["status"] != "NOT_FOUND" for c in cases)
|
| 369 |
+
yield _sse({
|
| 370 |
+
"type": "verify",
|
| 371 |
+
"grounded": grounded,
|
| 372 |
+
"flagged_sections": flagged_sections,
|
| 373 |
+
"retrieval_miss_sections": retrieval_miss,
|
| 374 |
+
"cases": cases,
|
| 375 |
+
})
|
| 376 |
+
|
| 377 |
+
return StreamingResponse(
|
| 378 |
+
event_stream(),
|
| 379 |
+
media_type="text/event-stream",
|
| 380 |
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
| 381 |
+
)
|
backend/build_index.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Build the `indian_statutes` ChromaDB collection from all_statutes.json.
|
| 3 |
+
|
| 4 |
+
Run once at Docker build time so the 19 MB binary index never has to live in
|
| 5 |
+
git (HF Spaces rejects >10 MB files without LFS, and rebuilding sidesteps any
|
| 6 |
+
chromadb on-disk-format version drift). Reproduces the original embeddings
|
| 7 |
+
faithfully: same model (BGE-small), same input (retrieval_text), same
|
| 8 |
+
normalization — see "statute corpus/create_embedding.py".
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import sys
|
| 14 |
+
|
| 15 |
+
import chromadb
|
| 16 |
+
from sentence_transformers import SentenceTransformer
|
| 17 |
+
|
| 18 |
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
PROJECT_ROOT = os.path.dirname(THIS_DIR)
|
| 20 |
+
STATUTE_DIR = os.path.join(PROJECT_ROOT, "statute corpus")
|
| 21 |
+
|
| 22 |
+
STATUTES_FILE = os.path.join(STATUTE_DIR, "all_statutes.json")
|
| 23 |
+
CHROMA_PATH = os.path.join(STATUTE_DIR, "chroma_statutes")
|
| 24 |
+
COLLECTION_NAME = "indian_statutes"
|
| 25 |
+
MODEL_NAME = "BAAI/bge-small-en-v1.5"
|
| 26 |
+
BATCH_SIZE = 100
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def main() -> None:
|
| 30 |
+
if not os.path.exists(STATUTES_FILE):
|
| 31 |
+
print(f"ERROR: {STATUTES_FILE} not found", file=sys.stderr)
|
| 32 |
+
sys.exit(1)
|
| 33 |
+
|
| 34 |
+
with open(STATUTES_FILE, "r", encoding="utf-8") as f:
|
| 35 |
+
statutes = json.load(f)
|
| 36 |
+
print(f"Loaded {len(statutes)} statute records")
|
| 37 |
+
|
| 38 |
+
model = SentenceTransformer(MODEL_NAME)
|
| 39 |
+
client = chromadb.PersistentClient(path=CHROMA_PATH)
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
client.delete_collection(COLLECTION_NAME)
|
| 43 |
+
except Exception:
|
| 44 |
+
pass
|
| 45 |
+
collection = client.create_collection(COLLECTION_NAME)
|
| 46 |
+
|
| 47 |
+
ids, documents, metadatas = [], [], []
|
| 48 |
+
for i, record in enumerate(statutes):
|
| 49 |
+
meta = record["metadata"]
|
| 50 |
+
# Reconstructed JSON has no chunk_id — synthesize a unique, stable id.
|
| 51 |
+
ids.append(f"{meta.get('act_short','?')}__{meta.get('section_number','?')}__{i}")
|
| 52 |
+
documents.append(record["retrieval_text"])
|
| 53 |
+
metadatas.append({
|
| 54 |
+
"act_short": str(meta.get("act_short", "")),
|
| 55 |
+
"act_name": str(meta.get("act_name", "")),
|
| 56 |
+
"section_number": str(meta.get("section_number", "")),
|
| 57 |
+
"title": str(meta.get("title", "")),
|
| 58 |
+
})
|
| 59 |
+
|
| 60 |
+
for i in range(0, len(documents), BATCH_SIZE):
|
| 61 |
+
batch_docs = documents[i:i + BATCH_SIZE]
|
| 62 |
+
embeddings = model.encode(batch_docs, normalize_embeddings=True, show_progress_bar=False)
|
| 63 |
+
collection.add(
|
| 64 |
+
ids=ids[i:i + BATCH_SIZE],
|
| 65 |
+
documents=batch_docs,
|
| 66 |
+
metadatas=metadatas[i:i + BATCH_SIZE],
|
| 67 |
+
embeddings=embeddings.tolist(),
|
| 68 |
+
)
|
| 69 |
+
print(f" embedded {min(i + BATCH_SIZE, len(documents))}/{len(documents)}")
|
| 70 |
+
|
| 71 |
+
print(f"DONE — {collection.count()} documents in '{COLLECTION_NAME}' at {CHROMA_PATH}")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
main()
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# themis backend — Phase 1 (statutes-only)
|
| 2 |
+
fastapi>=0.110
|
| 3 |
+
uvicorn[standard]>=0.29
|
| 4 |
+
pydantic>=2.6
|
| 5 |
+
|
| 6 |
+
# RAG pipeline (statutes + SCI judgments)
|
| 7 |
+
chromadb>=0.4.24
|
| 8 |
+
sentence-transformers>=2.6.0
|
| 9 |
+
openai>=1.30
|
| 10 |
+
python-dotenv>=1.0
|
| 11 |
+
rank-bm25>=0.2.2
|
| 12 |
+
huggingface_hub>=0.24
|
| 13 |
+
|
| 14 |
+
# Live citation verification (verifier/citation_verifier.py)
|
| 15 |
+
httpx>=0.27
|
| 16 |
+
ddddocr>=1.4
|
| 17 |
+
beautifulsoup4>=4.12
|
| 18 |
+
rapidfuzz>=3.6
|
chunking.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Parent-Child Hierarchical Chunker
|
| 3 |
+
=====================================================
|
| 4 |
+
Parent = full judgment text (used for context retrieval)
|
| 5 |
+
Child = fixed-token sub-chunks of parent (used for embedding + search)
|
| 6 |
+
|
| 7 |
+
Reads: data/html/extracted_judgments.jsonl (metadata)
|
| 8 |
+
data/pdfs/*.pdf (judgment text)
|
| 9 |
+
Writes: data/chunks/parent_child/<neutral_citation>.json
|
| 10 |
+
|
| 11 |
+
Architecture:
|
| 12 |
+
Query hits a child chunk (small, precise, embedded)
|
| 13 |
+
↓
|
| 14 |
+
Child carries parent_chunk_id
|
| 15 |
+
↓
|
| 16 |
+
Fetch parent for full context window
|
| 17 |
+
↓
|
| 18 |
+
Send parent text to LLM for answer generation
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import re
|
| 24 |
+
import fitz
|
| 25 |
+
import logging
|
| 26 |
+
import tiktoken
|
| 27 |
+
import concurrent.futures
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from datetime import datetime
|
| 30 |
+
|
| 31 |
+
# ---------------------------------------------------------------------------
|
| 32 |
+
# Logging
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
logging.basicConfig(
|
| 35 |
+
level=logging.INFO,
|
| 36 |
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
| 37 |
+
datefmt="%H:%M:%S",
|
| 38 |
+
handlers=[
|
| 39 |
+
logging.StreamHandler(),
|
| 40 |
+
logging.FileHandler("data/parent_child_chunker.log", encoding="utf-8"),
|
| 41 |
+
]
|
| 42 |
+
)
|
| 43 |
+
log = logging.getLogger(__name__)
|
| 44 |
+
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
# Config
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
METADATA_FILE = os.path.join("data", "html", "extracted_judgments.jsonl")
|
| 49 |
+
PDF_DIR = Path("data", "pdfs")
|
| 50 |
+
OUTPUT_DIR = Path("data", "chunks", "parent_child")
|
| 51 |
+
ERROR_LOG = Path("data", "chunks", "parent_child_errors.jsonl")
|
| 52 |
+
|
| 53 |
+
CHILD_CHUNK_SIZE = 512 # tokens per child chunk
|
| 54 |
+
CHILD_CHUNK_OVERLAP = 100 # token overlap between children
|
| 55 |
+
TOKENIZER_MODEL = "cl100k_base"
|
| 56 |
+
MAX_WORKERS = min(8, (os.cpu_count() or 4))
|
| 57 |
+
|
| 58 |
+
# Parent size threshold — if judgment is smaller than this, skip children
|
| 59 |
+
MIN_TOKENS_FOR_CHILDREN = CHILD_CHUNK_SIZE
|
| 60 |
+
|
| 61 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 62 |
+
|
| 63 |
+
# ---------------------------------------------------------------------------
|
| 64 |
+
# Globals (shared across threads — all read-only after init)
|
| 65 |
+
# ---------------------------------------------------------------------------
|
| 66 |
+
TOKENIZER = tiktoken.get_encoding(TOKENIZER_MODEL)
|
| 67 |
+
|
| 68 |
+
# Pre-compiled regex
|
| 69 |
+
RE_BACKSPACE = re.compile(r"\x08")
|
| 70 |
+
RE_AUTHOR = re.compile(r"\n\*\s*Author\n\d+\n")
|
| 71 |
+
RE_PAGE_NUMS = re.compile(r"\n\s*\d{1,3}\s*\n")
|
| 72 |
+
RE_FOOTER = re.compile(r"\nJudgment\s*/\s*Order of the Supreme Court\n?", re.I)
|
| 73 |
+
RE_HEADER = re.compile(r"\n(Supreme Court of India|IN THE SUPREME COURT OF INDIA)\n", re.I)
|
| 74 |
+
RE_NEWLINES = re.compile(r"\n{3,}")
|
| 75 |
+
RE_SPACES = re.compile(r"[ \t]+")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
# Step 1: Load metadata index keyed by PDF filename
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
def load_metadata_index(jsonl_path: str) -> dict:
|
| 82 |
+
"""
|
| 83 |
+
Returns:
|
| 84 |
+
{
|
| 85 |
+
"2026_INSC_479.pdf": { ...full metadata record... },
|
| 86 |
+
...
|
| 87 |
+
}
|
| 88 |
+
"""
|
| 89 |
+
index = {}
|
| 90 |
+
missing = 0
|
| 91 |
+
|
| 92 |
+
with open(jsonl_path, "r", encoding="utf-8") as f:
|
| 93 |
+
for line in f:
|
| 94 |
+
line = line.strip()
|
| 95 |
+
if not line:
|
| 96 |
+
continue
|
| 97 |
+
try:
|
| 98 |
+
record = json.loads(line)
|
| 99 |
+
except json.JSONDecodeError:
|
| 100 |
+
continue
|
| 101 |
+
|
| 102 |
+
pdf_path = record.get("pdf_path", "")
|
| 103 |
+
if pdf_path:
|
| 104 |
+
filename = Path(pdf_path).name # "2026_INSC_479.pdf"
|
| 105 |
+
index[filename] = record
|
| 106 |
+
else:
|
| 107 |
+
# Fallback: derive from neutral citation
|
| 108 |
+
nc = record.get("neutral_citation", "").strip()
|
| 109 |
+
if nc:
|
| 110 |
+
filename = nc.replace(" ", "_") + ".pdf"
|
| 111 |
+
index[filename] = record
|
| 112 |
+
missing += 1
|
| 113 |
+
|
| 114 |
+
log.info(f"Loaded {len(index)} metadata records "
|
| 115 |
+
f"({missing} used neutral_citation fallback).")
|
| 116 |
+
return index
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ---------------------------------------------------------------------------
|
| 120 |
+
# Step 2: PDF text extraction
|
| 121 |
+
# ---------------------------------------------------------------------------
|
| 122 |
+
def extract_pdf_text(pdf_path: Path) -> str:
|
| 123 |
+
"""Extract full text from PDF using pymupdf."""
|
| 124 |
+
parts = []
|
| 125 |
+
try:
|
| 126 |
+
doc = fitz.open(str(pdf_path))
|
| 127 |
+
for page in doc:
|
| 128 |
+
parts.append(page.get_text())
|
| 129 |
+
doc.close()
|
| 130 |
+
except Exception as e:
|
| 131 |
+
log.error(f"PDF read failed [{pdf_path.name}]: {e}")
|
| 132 |
+
return "\n".join(parts)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ---------------------------------------------------------------------------
|
| 136 |
+
# Step 3: Text cleaning
|
| 137 |
+
# ---------------------------------------------------------------------------
|
| 138 |
+
def clean_text(text: str) -> str:
|
| 139 |
+
"""Remove PDF artifacts, headers, footers, page numbers."""
|
| 140 |
+
if not text:
|
| 141 |
+
return ""
|
| 142 |
+
text = RE_BACKSPACE.sub("", text)
|
| 143 |
+
text = RE_AUTHOR.sub("\n", text)
|
| 144 |
+
text = RE_PAGE_NUMS.sub("\n\n", text)
|
| 145 |
+
text = RE_FOOTER.sub("\n", text)
|
| 146 |
+
text = RE_HEADER.sub("\n", text)
|
| 147 |
+
text = RE_NEWLINES.sub("\n\n", text)
|
| 148 |
+
text = RE_SPACES.sub(" ", text)
|
| 149 |
+
return text.strip()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# ---------------------------------------------------------------------------
|
| 153 |
+
# Step 4: Build lean metadata (for child chunks)
|
| 154 |
+
# ---------------------------------------------------------------------------
|
| 155 |
+
def build_lean_metadata(record: dict) -> dict:
|
| 156 |
+
"""
|
| 157 |
+
Child chunks carry only the fields needed for Qdrant payload filtering.
|
| 158 |
+
Full metadata lives on the parent — fetched at answer-generation time.
|
| 159 |
+
"""
|
| 160 |
+
return {
|
| 161 |
+
"case_name": record.get("case_name", ""),
|
| 162 |
+
"neutral_citation": record.get("neutral_citation", ""),
|
| 163 |
+
"date": record.get("date", ""),
|
| 164 |
+
"court": record.get("court", "Supreme Court"),
|
| 165 |
+
"case_type": record.get("case_type", ""),
|
| 166 |
+
"outcome": record.get("outcome", ""),
|
| 167 |
+
"acts": record.get("acts", []),
|
| 168 |
+
"keywords": record.get("keywords", []),
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ---------------------------------------------------------------------------
|
| 173 |
+
# Step 5: Build full metadata (for parent chunk)
|
| 174 |
+
# ---------------------------------------------------------------------------
|
| 175 |
+
def build_full_metadata(record: dict) -> dict:
|
| 176 |
+
return {
|
| 177 |
+
"case_name": record.get("case_name", ""),
|
| 178 |
+
"neutral_citation": record.get("neutral_citation", ""),
|
| 179 |
+
"appeal_no": record.get("appeal_no", ""),
|
| 180 |
+
"citation": record.get("citation", ""),
|
| 181 |
+
"date": record.get("date", ""),
|
| 182 |
+
"court": record.get("court", "Supreme Court"),
|
| 183 |
+
"lower_court": record.get("lower_court", ""),
|
| 184 |
+
"jurisdiction": record.get("jurisdiction", "India"),
|
| 185 |
+
"bench": record.get("bench", []),
|
| 186 |
+
"author_judge": record.get("author_judge", ""),
|
| 187 |
+
"outcome": record.get("outcome", ""),
|
| 188 |
+
"case_type": record.get("case_type", ""),
|
| 189 |
+
"acts": record.get("acts", []),
|
| 190 |
+
"sections": record.get("sections", []),
|
| 191 |
+
"cases_cited": record.get("cases_cited", []),
|
| 192 |
+
"keywords": record.get("keywords", []),
|
| 193 |
+
"issue": record.get("issue", ""),
|
| 194 |
+
"short_summary": record.get("short_summary", ""),
|
| 195 |
+
"full_headnote": record.get("full_headnote", ""),
|
| 196 |
+
"source_url": record.get("source_url", ""),
|
| 197 |
+
"scraped_at": record.get("scraped_at", ""),
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# ---------------------------------------------------------------------------
|
| 202 |
+
# Step 6: Core parent-child chunking logic
|
| 203 |
+
# ---------------------------------------------------------------------------
|
| 204 |
+
def build_parent_child(
|
| 205 |
+
neutral_citation: str,
|
| 206 |
+
cleaned_text: str,
|
| 207 |
+
full_metadata: dict,
|
| 208 |
+
lean_metadata: dict,
|
| 209 |
+
) -> dict:
|
| 210 |
+
"""
|
| 211 |
+
Returns:
|
| 212 |
+
{
|
| 213 |
+
"parent": { single parent chunk with full text + full metadata },
|
| 214 |
+
"children": [ child chunks with sub-text + lean metadata ]
|
| 215 |
+
}
|
| 216 |
+
"""
|
| 217 |
+
safe_nc = neutral_citation.replace(" ", "_")
|
| 218 |
+
parent_id = f"{safe_nc}__parent"
|
| 219 |
+
|
| 220 |
+
tokens = TOKENIZER.encode(cleaned_text)
|
| 221 |
+
num_tokens = len(tokens)
|
| 222 |
+
|
| 223 |
+
# --- Parent chunk ---
|
| 224 |
+
parent = {
|
| 225 |
+
"chunk_id": parent_id,
|
| 226 |
+
"chunk_type": "parent",
|
| 227 |
+
"document_id": neutral_citation,
|
| 228 |
+
"text": cleaned_text,
|
| 229 |
+
"token_count": num_tokens,
|
| 230 |
+
"char_count": len(cleaned_text),
|
| 231 |
+
"metadata": full_metadata,
|
| 232 |
+
"chunked_at": datetime.utcnow().isoformat(),
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
# --- Skip children if text is too small ---
|
| 236 |
+
if num_tokens <= MIN_TOKENS_FOR_CHILDREN:
|
| 237 |
+
parent["child_count"] = 0
|
| 238 |
+
return {"parent": parent, "children": []}
|
| 239 |
+
|
| 240 |
+
# --- Child chunks ---
|
| 241 |
+
step = CHILD_CHUNK_SIZE - CHILD_CHUNK_OVERLAP
|
| 242 |
+
children = []
|
| 243 |
+
idx = 0
|
| 244 |
+
|
| 245 |
+
for start in range(0, num_tokens, step):
|
| 246 |
+
end = min(start + CHILD_CHUNK_SIZE, num_tokens)
|
| 247 |
+
chunk_tokens = tokens[start:end]
|
| 248 |
+
chunk_text = TOKENIZER.decode(chunk_tokens)
|
| 249 |
+
|
| 250 |
+
children.append({
|
| 251 |
+
"chunk_id": f"{safe_nc}__child_{idx:04d}",
|
| 252 |
+
"chunk_type": "child",
|
| 253 |
+
"parent_chunk_id": parent_id,
|
| 254 |
+
"document_id": neutral_citation,
|
| 255 |
+
"child_index": idx,
|
| 256 |
+
"token_count": len(chunk_tokens),
|
| 257 |
+
"char_count": len(chunk_text),
|
| 258 |
+
"start_token": start,
|
| 259 |
+
"end_token": end,
|
| 260 |
+
"text": chunk_text,
|
| 261 |
+
"metadata": lean_metadata,
|
| 262 |
+
})
|
| 263 |
+
idx += 1
|
| 264 |
+
|
| 265 |
+
if end == num_tokens:
|
| 266 |
+
break
|
| 267 |
+
|
| 268 |
+
parent["child_count"] = len(children)
|
| 269 |
+
return {"parent": parent, "children": children}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
# ---------------------------------------------------------------------------
|
| 273 |
+
# Step 7: Process single PDF (called by thread pool)
|
| 274 |
+
# ---------------------------------------------------------------------------
|
| 275 |
+
def process_pdf(pdf_path: Path, metadata_index: dict) -> dict:
|
| 276 |
+
"""
|
| 277 |
+
Returns a result dict:
|
| 278 |
+
{
|
| 279 |
+
"status": "success" | "skipped" | "error",
|
| 280 |
+
"file": pdf filename,
|
| 281 |
+
"message": description,
|
| 282 |
+
"chunks": { parent, children } or None
|
| 283 |
+
}
|
| 284 |
+
"""
|
| 285 |
+
filename = pdf_path.name
|
| 286 |
+
|
| 287 |
+
# --- Idempotency: skip if already processed ---
|
| 288 |
+
record = metadata_index.get(filename)
|
| 289 |
+
if not record:
|
| 290 |
+
return {"status": "unmatched", "file": filename,
|
| 291 |
+
"message": f"No metadata found for {filename}"}
|
| 292 |
+
|
| 293 |
+
neutral_citation = record.get("neutral_citation", "")
|
| 294 |
+
safe_nc = neutral_citation.replace(" ", "_")
|
| 295 |
+
output_file = OUTPUT_DIR / f"{safe_nc}.json"
|
| 296 |
+
|
| 297 |
+
if output_file.exists():
|
| 298 |
+
return {"status": "skipped", "file": filename,
|
| 299 |
+
"message": f"Already processed: {output_file.name}"}
|
| 300 |
+
|
| 301 |
+
# --- Extract + clean text ---
|
| 302 |
+
raw_text = extract_pdf_text(pdf_path)
|
| 303 |
+
cleaned = clean_text(raw_text)
|
| 304 |
+
|
| 305 |
+
if not cleaned:
|
| 306 |
+
return {"status": "error", "file": filename,
|
| 307 |
+
"message": "Empty text after cleaning"}
|
| 308 |
+
|
| 309 |
+
# --- Build metadata ---
|
| 310 |
+
full_meta = build_full_metadata(record)
|
| 311 |
+
lean_meta = build_lean_metadata(record)
|
| 312 |
+
|
| 313 |
+
# --- Build parent-child structure ---
|
| 314 |
+
result = build_parent_child(neutral_citation, cleaned, full_meta, lean_meta)
|
| 315 |
+
|
| 316 |
+
# --- Write output atomically ---
|
| 317 |
+
# Write to temp file first, then rename — prevents corrupt files on crash
|
| 318 |
+
temp_file = output_file.with_suffix(".tmp")
|
| 319 |
+
try:
|
| 320 |
+
with open(temp_file, "w", encoding="utf-8") as f:
|
| 321 |
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
| 322 |
+
temp_file.rename(output_file)
|
| 323 |
+
except Exception as e:
|
| 324 |
+
if temp_file.exists():
|
| 325 |
+
temp_file.unlink()
|
| 326 |
+
return {"status": "error", "file": filename, "message": str(e)}
|
| 327 |
+
|
| 328 |
+
return {
|
| 329 |
+
"status": "success",
|
| 330 |
+
"file": filename,
|
| 331 |
+
"message": f"{result['parent']['child_count']} children created",
|
| 332 |
+
"children": result["parent"]["child_count"],
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# ---------------------------------------------------------------------------
|
| 337 |
+
# Step 8: Main pipeline
|
| 338 |
+
# ---------------------------------------------------------------------------
|
| 339 |
+
def main():
|
| 340 |
+
log.info("=" * 60)
|
| 341 |
+
log.info("Parent-Child Chunker — Starting")
|
| 342 |
+
log.info("=" * 60)
|
| 343 |
+
|
| 344 |
+
# Load metadata
|
| 345 |
+
metadata_index = load_metadata_index(METADATA_FILE)
|
| 346 |
+
|
| 347 |
+
# Discover PDFs
|
| 348 |
+
pdf_files = sorted(PDF_DIR.glob("*.pdf"))
|
| 349 |
+
log.info(f"Found {len(pdf_files)} PDFs in {PDF_DIR}")
|
| 350 |
+
|
| 351 |
+
if not pdf_files:
|
| 352 |
+
log.error("No PDFs found. Check PDF_DIR path.")
|
| 353 |
+
return
|
| 354 |
+
|
| 355 |
+
# Counters
|
| 356 |
+
counts = {"success": 0, "skipped": 0, "unmatched": 0, "error": 0}
|
| 357 |
+
total_children = 0
|
| 358 |
+
errors = []
|
| 359 |
+
|
| 360 |
+
# Process concurrently
|
| 361 |
+
log.info(f"Processing with {MAX_WORKERS} workers...")
|
| 362 |
+
|
| 363 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
| 364 |
+
futures = {
|
| 365 |
+
executor.submit(process_pdf, pdf_path, metadata_index): pdf_path
|
| 366 |
+
for pdf_path in pdf_files
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
for i, future in enumerate(concurrent.futures.as_completed(futures), 1):
|
| 370 |
+
pdf_path = futures[future]
|
| 371 |
+
try:
|
| 372 |
+
result = future.result()
|
| 373 |
+
status = result["status"]
|
| 374 |
+
counts[status] = counts.get(status, 0) + 1
|
| 375 |
+
|
| 376 |
+
if status == "success":
|
| 377 |
+
total_children += result.get("children", 0)
|
| 378 |
+
if i % 50 == 0:
|
| 379 |
+
log.info(
|
| 380 |
+
f"Progress: {i}/{len(pdf_files)} | "
|
| 381 |
+
f"Success: {counts['success']} | "
|
| 382 |
+
f"Skipped: {counts['skipped']} | "
|
| 383 |
+
f"Errors: {counts['error']}"
|
| 384 |
+
)
|
| 385 |
+
elif status == "error":
|
| 386 |
+
log.warning(f"[ERROR] {result['file']}: {result['message']}")
|
| 387 |
+
errors.append(result)
|
| 388 |
+
elif status == "unmatched":
|
| 389 |
+
log.warning(f"[UNMATCHED] {result['file']}")
|
| 390 |
+
errors.append(result)
|
| 391 |
+
|
| 392 |
+
except Exception as exc:
|
| 393 |
+
counts["error"] += 1
|
| 394 |
+
log.error(f"[EXCEPTION] {pdf_path.name}: {exc}")
|
| 395 |
+
errors.append({"file": pdf_path.name, "message": str(exc)})
|
| 396 |
+
|
| 397 |
+
# Write error log
|
| 398 |
+
if errors:
|
| 399 |
+
with open(ERROR_LOG, "w", encoding="utf-8") as f:
|
| 400 |
+
for e in errors:
|
| 401 |
+
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
| 402 |
+
log.info(f"Error details → {ERROR_LOG}")
|
| 403 |
+
|
| 404 |
+
# Final summary
|
| 405 |
+
log.info("=" * 60)
|
| 406 |
+
log.info("PIPELINE COMPLETE")
|
| 407 |
+
log.info(f" Successful : {counts['success']}")
|
| 408 |
+
log.info(f" Skipped : {counts['skipped']} (already processed)")
|
| 409 |
+
log.info(f" Unmatched : {counts['unmatched']} (no metadata)")
|
| 410 |
+
log.info(f" Errors : {counts['error']}")
|
| 411 |
+
log.info(f" Total children created : {total_children}")
|
| 412 |
+
log.info(f" Output dir : {OUTPUT_DIR.resolve()}")
|
| 413 |
+
log.info("=" * 60)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
if __name__ == "__main__":
|
| 417 |
+
main()
|
embedding.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#%%
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import chromadb
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
|
| 8 |
+
CHUNKS_DIR = "/content/drive/MyDrive/updatedparentchunk/chunks/parent_child"
|
| 9 |
+
|
| 10 |
+
client = chromadb.PersistentClient(
|
| 11 |
+
path="/content/drive/MyDrive/chroma_bge_v2"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
collection = client.get_or_create_collection(
|
| 15 |
+
"sci_judgments_bge_v2"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
model = SentenceTransformer(
|
| 19 |
+
"BAAI/bge-small-en-v1.5"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
def flatten_metadata(meta):
|
| 23 |
+
flat = {}
|
| 24 |
+
|
| 25 |
+
for k, v in meta.items():
|
| 26 |
+
|
| 27 |
+
if isinstance(v, (str, int, float, bool)):
|
| 28 |
+
flat[k] = v
|
| 29 |
+
|
| 30 |
+
elif isinstance(v, (list, dict)):
|
| 31 |
+
flat[k] = json.dumps(v)
|
| 32 |
+
|
| 33 |
+
else:
|
| 34 |
+
flat[k] = str(v)
|
| 35 |
+
|
| 36 |
+
return flat
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
files = [
|
| 40 |
+
f for f in os.listdir(CHUNKS_DIR)
|
| 41 |
+
if f.endswith(".json")
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
print("Files:", len(files))
|
| 45 |
+
|
| 46 |
+
all_ids = []
|
| 47 |
+
all_docs = []
|
| 48 |
+
all_meta = []
|
| 49 |
+
|
| 50 |
+
for filename in tqdm(files):
|
| 51 |
+
|
| 52 |
+
with open(
|
| 53 |
+
os.path.join(CHUNKS_DIR, filename),
|
| 54 |
+
encoding="utf-8"
|
| 55 |
+
) as f:
|
| 56 |
+
|
| 57 |
+
data = json.load(f)
|
| 58 |
+
|
| 59 |
+
parent = data["parent"]
|
| 60 |
+
|
| 61 |
+
all_ids.append(parent["chunk_id"])
|
| 62 |
+
all_docs.append(parent["text"])
|
| 63 |
+
all_meta.append(
|
| 64 |
+
flatten_metadata(parent["metadata"])
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
for child in data["children"]:
|
| 68 |
+
|
| 69 |
+
all_ids.append(child["chunk_id"])
|
| 70 |
+
all_docs.append(child["text"])
|
| 71 |
+
all_meta.append(
|
| 72 |
+
flatten_metadata(child["metadata"])
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
print("Total chunks:", len(all_ids))
|
| 76 |
+
|
| 77 |
+
BATCH_SIZE = 512
|
| 78 |
+
|
| 79 |
+
for start in tqdm(
|
| 80 |
+
range(0, len(all_ids), BATCH_SIZE),
|
| 81 |
+
desc="Embedding"
|
| 82 |
+
):
|
| 83 |
+
|
| 84 |
+
end = min(
|
| 85 |
+
start + BATCH_SIZE,
|
| 86 |
+
len(all_ids)
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
batch_docs = all_docs[start:end]
|
| 90 |
+
|
| 91 |
+
embeddings = model.encode(
|
| 92 |
+
batch_docs,
|
| 93 |
+
batch_size=128,
|
| 94 |
+
normalize_embeddings=True,
|
| 95 |
+
show_progress_bar=False
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
collection.add(
|
| 99 |
+
ids=all_ids[start:end],
|
| 100 |
+
embeddings=embeddings.tolist(),
|
| 101 |
+
documents=batch_docs,
|
| 102 |
+
metadatas=all_meta[start:end]
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
print("\nDONE")
|
| 106 |
+
print("Collection count:", collection.count())
|
hybrid_rag/unified_legal_Rag.py
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
unified_legal_rag.py
|
| 3 |
+
=====================
|
| 4 |
+
Unified Legal RAG Router — LegalAIapex
|
| 5 |
+
|
| 6 |
+
Connects two existing, independently-built retrieval pipelines:
|
| 7 |
+
1. Statute retrieval (statute_retrieval.py) — IPC/BNS/CrPC/BNSS/IEA/BSA
|
| 8 |
+
2. Judgment retrieval (judgment_retrieval.py) — SCI judgment corpus
|
| 9 |
+
|
| 10 |
+
Architecture:
|
| 11 |
+
|
| 12 |
+
Query
|
| 13 |
+
|
|
| 14 |
+
v
|
| 15 |
+
Query Router (decides: statute | judgment | hybrid)
|
| 16 |
+
|
|
| 17 |
+
+----------------+----------------+
|
| 18 |
+
| | |
|
| 19 |
+
Statute Path Judgment Path Hybrid Path
|
| 20 |
+
(direct lookup (citation / (BOTH retrieved
|
| 21 |
+
+ semantic) comparison / in parallel)
|
| 22 |
+
semantic)
|
| 23 |
+
| | |
|
| 24 |
+
+----------------+----------------+
|
| 25 |
+
|
|
| 26 |
+
v
|
| 27 |
+
Unified Cross-Encoder Rerank
|
| 28 |
+
(statute + judgment candidates
|
| 29 |
+
scored together, top-N kept)
|
| 30 |
+
|
|
| 31 |
+
v
|
| 32 |
+
Build Combined Context
|
| 33 |
+
|
|
| 34 |
+
v
|
| 35 |
+
DeepSeek
|
| 36 |
+
|
|
| 37 |
+
v
|
| 38 |
+
Answer
|
| 39 |
+
|
| 40 |
+
Design notes:
|
| 41 |
+
- Both source pipelines are imported as modules, NOT rewritten.
|
| 42 |
+
This file only adds a routing + fusion layer on top.
|
| 43 |
+
- Each source's own internal logic (direct lookup, citation search,
|
| 44 |
+
comparison mode, query expansion) is preserved and called as-is.
|
| 45 |
+
- The unified reranker re-scores statute and judgment candidates
|
| 46 |
+
on the SAME scale so they can be merged fairly — this is the
|
| 47 |
+
critical step that makes "best evidence overall" meaningful.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
import logging
|
| 51 |
+
import os
|
| 52 |
+
import re
|
| 53 |
+
import sys
|
| 54 |
+
import time
|
| 55 |
+
from dataclasses import dataclass, field
|
| 56 |
+
from typing import Optional
|
| 57 |
+
|
| 58 |
+
from dotenv import load_dotenv
|
| 59 |
+
|
| 60 |
+
# ---------------------------------------------------------------------
|
| 61 |
+
# Path setup — both retrieval pipelines live in sibling folders relative
|
| 62 |
+
# to this file (hybrid rag/), not in this folder. We add both to
|
| 63 |
+
# sys.path BEFORE importing them, and load the shared .env from the
|
| 64 |
+
# project root so DEEPSEEK_API_KEY is available to both pipelines.
|
| 65 |
+
# ---------------------------------------------------------------------
|
| 66 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 67 |
+
PROJECT_ROOT = os.path.join(BASE_DIR, "..")
|
| 68 |
+
|
| 69 |
+
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
|
| 70 |
+
|
| 71 |
+
sys.path.insert(0, os.path.join(PROJECT_ROOT, "statute corpus"))
|
| 72 |
+
sys.path.insert(0, PROJECT_ROOT) # so the verifier package + llm_retriever are importable
|
| 73 |
+
|
| 74 |
+
# ---------------------------------------------------------------------
|
| 75 |
+
# Import both existing pipelines as modules.
|
| 76 |
+
# statute pipeline -> "statute corpus/statute_retrieval.py"
|
| 77 |
+
# judgment pipeline -> "llm_retriever.py" (project root)
|
| 78 |
+
# ---------------------------------------------------------------------
|
| 79 |
+
import statute_retrieval as stat_rag
|
| 80 |
+
import llm_retriever as judg_rag
|
| 81 |
+
|
| 82 |
+
# NOTE: verification is handled in the backend (backend/app.py) via the live
|
| 83 |
+
# citation verifier (verifier/citation_verifier.py). The old grounding-based
|
| 84 |
+
# verify_answer was removed; this module's CLI ask() no longer verifies.
|
| 85 |
+
|
| 86 |
+
# =====================================================================
|
| 87 |
+
# LOGGING
|
| 88 |
+
# =====================================================================
|
| 89 |
+
|
| 90 |
+
log = logging.getLogger("unified_rag")
|
| 91 |
+
logging.basicConfig(
|
| 92 |
+
level=logging.INFO,
|
| 93 |
+
format="%(asctime)s %(levelname)-8s %(message)s",
|
| 94 |
+
datefmt="%H:%M:%S",
|
| 95 |
+
handlers=[logging.StreamHandler(sys.stdout)],
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# =====================================================================
|
| 99 |
+
# CONFIG
|
| 100 |
+
# =====================================================================
|
| 101 |
+
|
| 102 |
+
# How many candidates to pull from EACH source before unified rerank
|
| 103 |
+
STATUTE_CANDIDATES_N = 15
|
| 104 |
+
JUDGMENT_CANDIDATES_N = 15
|
| 105 |
+
|
| 106 |
+
# Final number of evidence blocks sent to DeepSeek after fusion
|
| 107 |
+
FINAL_EVIDENCE_N = 8
|
| 108 |
+
|
| 109 |
+
# Keep at most this many of one type if the other type is starved
|
| 110 |
+
# (prevents one source from completely drowning out the other)
|
| 111 |
+
MAX_SINGLE_TYPE_SHARE = 6
|
| 112 |
+
|
| 113 |
+
LLM_MODEL = "deepseek-chat"
|
| 114 |
+
LLM_TEMPERATURE = 0.1
|
| 115 |
+
LLM_MAX_TOKENS = 2500
|
| 116 |
+
|
| 117 |
+
# =====================================================================
|
| 118 |
+
# QUERY ROUTING
|
| 119 |
+
# =====================================================================
|
| 120 |
+
|
| 121 |
+
# Words that signal the user wants case-law / judgment material
|
| 122 |
+
JUDGMENT_SIGNAL_RE = re.compile(
|
| 123 |
+
r"\b(case|judgment|judgement|held|ruling|precedent|"
|
| 124 |
+
r"supreme court|high court|bench|justice|"
|
| 125 |
+
r"insc|scc|s\.c\.r|s\.c\.c|cited|"
|
| 126 |
+
r"per\s+incuriam|ratio|obiter|overrul)\b",
|
| 127 |
+
re.IGNORECASE,
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# Words that signal the user wants statute / bare-act text
|
| 131 |
+
STATUTE_SIGNAL_RE = re.compile(
|
| 132 |
+
r"\b(section|provision|act\b|sanhita|adhiniyam|"
|
| 133 |
+
r"punishment for|definition of|what is the law on|"
|
| 134 |
+
r"ipc|bns\b|crpc|bnss|iea|bsa)\b",
|
| 135 |
+
re.IGNORECASE,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@dataclass
|
| 140 |
+
class RouteDecision:
|
| 141 |
+
use_statute: bool
|
| 142 |
+
use_judgment: bool
|
| 143 |
+
reason: str
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def route_query(query: str) -> RouteDecision:
|
| 147 |
+
"""
|
| 148 |
+
Decide which retriever(s) to call.
|
| 149 |
+
|
| 150 |
+
Default behaviour is HYBRID — both retrievers run, because most
|
| 151 |
+
real legal questions benefit from both statute text and case law
|
| 152 |
+
interpreting it. We only skip a source when the query is clearly
|
| 153 |
+
and exclusively about the other domain.
|
| 154 |
+
"""
|
| 155 |
+
has_citation = bool(judg_rag.extract_citations(query))
|
| 156 |
+
judgment_signal = bool(JUDGMENT_SIGNAL_RE.search(query))
|
| 157 |
+
statute_signal = bool(STATUTE_SIGNAL_RE.search(query))
|
| 158 |
+
section_refs = stat_rag.parse_section_references(query)
|
| 159 |
+
|
| 160 |
+
# Explicit citation (e.g. "2025 INSC 337") → judgment-only,
|
| 161 |
+
# this is an exact lookup, statute search would add noise
|
| 162 |
+
if has_citation and not statute_signal:
|
| 163 |
+
return RouteDecision(
|
| 164 |
+
use_statute=False, use_judgment=True,
|
| 165 |
+
reason="explicit case citation detected",
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# Pure section reference with no case-law language → statute-only
|
| 169 |
+
if section_refs and not judgment_signal:
|
| 170 |
+
return RouteDecision(
|
| 171 |
+
use_statute=True, use_judgment=False,
|
| 172 |
+
reason="pure section reference, no case-law signal",
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
# Both signals present, or neither (ambiguous) → hybrid
|
| 176 |
+
return RouteDecision(
|
| 177 |
+
use_statute=True, use_judgment=True,
|
| 178 |
+
reason="hybrid: both or neither signal strongly present",
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# =====================================================================
|
| 183 |
+
# UNIFIED CANDIDATE TYPE
|
| 184 |
+
# =====================================================================
|
| 185 |
+
|
| 186 |
+
@dataclass
|
| 187 |
+
class Evidence:
|
| 188 |
+
"""A single piece of evidence, normalised across both sources."""
|
| 189 |
+
source_type: str # "statute" | "judgment"
|
| 190 |
+
score: float # raw score from source-specific rerank
|
| 191 |
+
unified_score: float = 0.0 # score after unified rerank — set later
|
| 192 |
+
rerank_text: str = "" # text used for unified reranking
|
| 193 |
+
display_block: str = "" # pre-formatted text for LLM context
|
| 194 |
+
meta: dict = field(default_factory=dict)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# =====================================================================
|
| 198 |
+
# STATUTE PATH
|
| 199 |
+
# =====================================================================
|
| 200 |
+
|
| 201 |
+
def get_statute_evidence(query: str) -> list[Evidence]:
|
| 202 |
+
"""
|
| 203 |
+
Runs the statute pipeline's own classification + direct lookup +
|
| 204 |
+
semantic search + its own rerank, then wraps results as Evidence.
|
| 205 |
+
Direct-lookup hits are kept separately so they always survive
|
| 206 |
+
into the unified rerank with a strong prior.
|
| 207 |
+
"""
|
| 208 |
+
intent = stat_rag.classify_query(query)
|
| 209 |
+
|
| 210 |
+
direct_records = []
|
| 211 |
+
for act, sec in intent["section_refs"]:
|
| 212 |
+
rec = stat_rag.direct_lookup(act, sec)
|
| 213 |
+
if rec:
|
| 214 |
+
direct_records.append(rec)
|
| 215 |
+
|
| 216 |
+
expanded = (
|
| 217 |
+
stat_rag.expand_query(query)
|
| 218 |
+
if intent["type"] in ("semantic", "hybrid")
|
| 219 |
+
else [query]
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
rerank_query = query
|
| 223 |
+
if expanded and expanded[0] != query:
|
| 224 |
+
rerank_query = f"{query}. {' '.join(expanded[:2])}"
|
| 225 |
+
|
| 226 |
+
candidates = stat_rag.semantic_search(query)
|
| 227 |
+
|
| 228 |
+
existing_keys = {
|
| 229 |
+
(m.get("act_short", ""), str(m.get("section_number", "")))
|
| 230 |
+
for _, _, m in candidates
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
for exp_q in expanded:
|
| 234 |
+
if exp_q == query:
|
| 235 |
+
continue
|
| 236 |
+
for item in stat_rag.semantic_search(exp_q, top_k=20):
|
| 237 |
+
key = (item[2].get("act_short", ""), str(item[2].get("section_number", "")))
|
| 238 |
+
if key not in existing_keys:
|
| 239 |
+
candidates.append(item)
|
| 240 |
+
existing_keys.add(key)
|
| 241 |
+
|
| 242 |
+
if intent["acts_mentioned"]:
|
| 243 |
+
for act in intent["acts_mentioned"]:
|
| 244 |
+
for item in stat_rag.semantic_search(query, top_k=15, act_filter=act):
|
| 245 |
+
key = (item[2].get("act_short", ""), str(item[2].get("section_number", "")))
|
| 246 |
+
if key not in existing_keys:
|
| 247 |
+
candidates.append(item)
|
| 248 |
+
existing_keys.add(key)
|
| 249 |
+
|
| 250 |
+
if intent["is_comparative"]:
|
| 251 |
+
for act in list(intent["acts_mentioned"]):
|
| 252 |
+
equiv = stat_rag.ACT_EQUIVALENTS.get(act.upper())
|
| 253 |
+
if equiv:
|
| 254 |
+
for item in stat_rag.semantic_search(query, top_k=15, act_filter=equiv):
|
| 255 |
+
key = (item[2].get("act_short", ""), str(item[2].get("section_number", "")))
|
| 256 |
+
if key not in existing_keys:
|
| 257 |
+
candidates.append(item)
|
| 258 |
+
existing_keys.add(key)
|
| 259 |
+
|
| 260 |
+
ranked = stat_rag.rerank_results(
|
| 261 |
+
rerank_query, candidates, top_k=STATUTE_CANDIDATES_N
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
evidence: list[Evidence] = []
|
| 265 |
+
|
| 266 |
+
# Direct lookups — always included, marked with strong prior score
|
| 267 |
+
for rec in direct_records:
|
| 268 |
+
meta = rec["metadata"]
|
| 269 |
+
text = rec.get(
|
| 270 |
+
"retrieval_text",
|
| 271 |
+
rec.get("content_payload", {}).get("text", ""),
|
| 272 |
+
)
|
| 273 |
+
evidence.append(Evidence(
|
| 274 |
+
source_type="statute",
|
| 275 |
+
score=999.0, # exact match — always wins ties pre-rerank
|
| 276 |
+
rerank_text=text,
|
| 277 |
+
display_block=(
|
| 278 |
+
f"--- STATUTE [DIRECT MATCH] ---\n"
|
| 279 |
+
f"Act: {meta['act_name']} ({meta['act_short']})\n"
|
| 280 |
+
f"Section: {meta['section_number']}\n"
|
| 281 |
+
f"Title: {meta.get('title','')}\n\n{text}\n"
|
| 282 |
+
),
|
| 283 |
+
meta=meta,
|
| 284 |
+
))
|
| 285 |
+
|
| 286 |
+
for score, doc, meta in ranked:
|
| 287 |
+
evidence.append(Evidence(
|
| 288 |
+
source_type="statute",
|
| 289 |
+
score=score,
|
| 290 |
+
rerank_text=doc,
|
| 291 |
+
display_block=(
|
| 292 |
+
f"--- STATUTE [score {score:.3f}] ---\n"
|
| 293 |
+
f"Act: {meta.get('act_name','')} ({meta.get('act_short','')})\n"
|
| 294 |
+
f"Section: {meta.get('section_number','')}\n"
|
| 295 |
+
f"Title: {meta.get('title','')}\n\n{doc}\n"
|
| 296 |
+
),
|
| 297 |
+
meta=meta,
|
| 298 |
+
))
|
| 299 |
+
|
| 300 |
+
log.info("Statute path: %d evidence items (%d direct)", len(evidence), len(direct_records))
|
| 301 |
+
return evidence
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
# =====================================================================
|
| 305 |
+
# JUDGMENT PATH
|
| 306 |
+
# =====================================================================
|
| 307 |
+
|
| 308 |
+
def _build_severity_context(query: str) -> str:
|
| 309 |
+
"""
|
| 310 |
+
If the query references a specific statute section, fetch that
|
| 311 |
+
section's title (e.g. "Punishment for murder") and append it to
|
| 312 |
+
the judgment search query.
|
| 313 |
+
|
| 314 |
+
Why this matters: generic queries like "bail in cases under BNS 103"
|
| 315 |
+
share a lot of surface vocabulary ("bail", "BNSS 480", "section")
|
| 316 |
+
with judgments about completely different, less severe offences
|
| 317 |
+
(e.g. a tenancy-dispute cheating case). The cross-encoder reranker
|
| 318 |
+
scores surface/topical similarity, so without this enrichment a
|
| 319 |
+
legally irrelevant but vocabulary-similar judgment can outscore the
|
| 320 |
+
actually relevant one. Appending the offence title biases retrieval
|
| 321 |
+
and reranking toward judgments discussing that same severity class.
|
| 322 |
+
"""
|
| 323 |
+
section_refs = stat_rag.parse_section_references(query)
|
| 324 |
+
if not section_refs:
|
| 325 |
+
return ""
|
| 326 |
+
|
| 327 |
+
titles = []
|
| 328 |
+
for act, sec in section_refs:
|
| 329 |
+
record = stat_rag.direct_lookup(act, sec)
|
| 330 |
+
if record:
|
| 331 |
+
title = record["metadata"].get("title", "")
|
| 332 |
+
if title:
|
| 333 |
+
titles.append(title)
|
| 334 |
+
|
| 335 |
+
return " ".join(titles)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
JUDGMENT_EXPANSION_PROMPT = """\
|
| 339 |
+
You are an expert Indian Supreme Court legal researcher.
|
| 340 |
+
Given a user's question, extract the core legal issues and rewrite it into a dense string of search terms likely to be found in Supreme Court headnotes, ratio decidendi, and legal doctrines.
|
| 341 |
+
Do not write sentences. Just output a space-separated list of highly relevant legal keywords, maxims, and statutory references.
|
| 342 |
+
For example, if the user asks "Can a dying declaration alone be the basis for conviction without corroboration?", output: "dying declaration corroboration sole basis of conviction evidentiary value section 32 indian evidence act"
|
| 343 |
+
Output ONLY the keywords, nothing else.\
|
| 344 |
+
"""
|
| 345 |
+
|
| 346 |
+
def _expand_judgment_query(query: str) -> str:
|
| 347 |
+
"""
|
| 348 |
+
Uses the DeepSeek LLM to translate a natural language question into
|
| 349 |
+
dense legal search terms optimized for Supreme Court judgment retrieval.
|
| 350 |
+
"""
|
| 351 |
+
# Don't expand if it's just a raw citation (e.g. "2025 INSC 337")
|
| 352 |
+
if bool(judg_rag.extract_citations(query)) and len(query.split()) < 5:
|
| 353 |
+
return ""
|
| 354 |
+
|
| 355 |
+
try:
|
| 356 |
+
response = stat_rag.llm_client.chat.completions.create(
|
| 357 |
+
model=LLM_MODEL,
|
| 358 |
+
messages=[
|
| 359 |
+
{"role": "system", "content": JUDGMENT_EXPANSION_PROMPT},
|
| 360 |
+
{"role": "user", "content": query},
|
| 361 |
+
],
|
| 362 |
+
temperature=0.0,
|
| 363 |
+
max_tokens=100,
|
| 364 |
+
)
|
| 365 |
+
expanded = response.choices[0].message.content.strip()
|
| 366 |
+
|
| 367 |
+
# Strip out any chatty prefix if the model ignored instructions
|
| 368 |
+
if ":" in expanded[:20]:
|
| 369 |
+
expanded = expanded.split(":", 1)[1].strip()
|
| 370 |
+
|
| 371 |
+
log.info("Agentic judgment expansion: '%s'", expanded)
|
| 372 |
+
return expanded
|
| 373 |
+
except Exception as e:
|
| 374 |
+
log.warning("Agentic judgment expansion failed: %s", e)
|
| 375 |
+
return ""
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def get_judgment_evidence(query: str) -> list[Evidence]:
|
| 379 |
+
"""
|
| 380 |
+
Runs the judgment pipeline's own retrieve() — which internally
|
| 381 |
+
handles citation lookup, comparison mode, and hybrid semantic
|
| 382 |
+
search with dense+BM25+RRF+parent fetch — then wraps the
|
| 383 |
+
resulting parent documents as Evidence.
|
| 384 |
+
|
| 385 |
+
The query is enriched in two ways before search:
|
| 386 |
+
1. Agentic Expansion: LLM translates query to legal keywords/doctrines.
|
| 387 |
+
2. Severity Context: If a statute is referenced, its title is appended.
|
| 388 |
+
"""
|
| 389 |
+
severity_context = _build_severity_context(query)
|
| 390 |
+
agent_expansion = _expand_judgment_query(query)
|
| 391 |
+
|
| 392 |
+
parts = [query]
|
| 393 |
+
if severity_context:
|
| 394 |
+
parts.append(severity_context)
|
| 395 |
+
if agent_expansion:
|
| 396 |
+
parts.append(agent_expansion)
|
| 397 |
+
|
| 398 |
+
search_query = " ".join(parts)
|
| 399 |
+
|
| 400 |
+
if len(parts) > 1:
|
| 401 |
+
log.info("Judgment query enriched to: '%s'", search_query)
|
| 402 |
+
|
| 403 |
+
result = judg_rag.retrieve(search_query)
|
| 404 |
+
|
| 405 |
+
if result is None:
|
| 406 |
+
log.info("Judgment path: no results")
|
| 407 |
+
return []
|
| 408 |
+
|
| 409 |
+
rtype = result.get("type", "")
|
| 410 |
+
evidence: list[Evidence] = []
|
| 411 |
+
|
| 412 |
+
# Single citation lookup → one parent doc, treat as a direct hit
|
| 413 |
+
if rtype in ("citation", "case"):
|
| 414 |
+
meta = result["metadata"]
|
| 415 |
+
text = result["document"]
|
| 416 |
+
rerank_text = " ".join(filter(None, [
|
| 417 |
+
meta.get("short_summary", ""),
|
| 418 |
+
meta.get("issue", ""),
|
| 419 |
+
text[:1000],
|
| 420 |
+
]))
|
| 421 |
+
evidence.append(Evidence(
|
| 422 |
+
source_type="judgment",
|
| 423 |
+
score=999.0, # exact citation match — always wins ties
|
| 424 |
+
rerank_text=rerank_text,
|
| 425 |
+
display_block=_format_judgment_block(meta, text, exact=True),
|
| 426 |
+
meta=meta,
|
| 427 |
+
))
|
| 428 |
+
return evidence
|
| 429 |
+
|
| 430 |
+
# Comparison or semantic — multiple parent docs already reranked
|
| 431 |
+
# by the judgment pipeline's own rerank_parents()
|
| 432 |
+
for case in result.get("parents", []):
|
| 433 |
+
meta = case["metadata"]
|
| 434 |
+
text = case["document"]
|
| 435 |
+
rerank_text = " ".join(filter(None, [
|
| 436 |
+
meta.get("short_summary", ""),
|
| 437 |
+
meta.get("issue", ""),
|
| 438 |
+
text[:1000],
|
| 439 |
+
]))
|
| 440 |
+
evidence.append(Evidence(
|
| 441 |
+
source_type="judgment",
|
| 442 |
+
score=0.0, # will be re-scored by unified reranker
|
| 443 |
+
rerank_text=rerank_text,
|
| 444 |
+
display_block=_format_judgment_block(meta, text, exact=False),
|
| 445 |
+
meta=meta,
|
| 446 |
+
))
|
| 447 |
+
|
| 448 |
+
log.info("Judgment path: %d evidence items (mode=%s)", len(evidence), rtype)
|
| 449 |
+
return evidence[:JUDGMENT_CANDIDATES_N]
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def _format_judgment_block(meta: dict, text: str, exact: bool) -> str:
|
| 453 |
+
tag = "DIRECT CITATION MATCH" if exact else "RELEVANT JUDGMENT"
|
| 454 |
+
return (
|
| 455 |
+
f"--- JUDGMENT [{tag}] ---\n"
|
| 456 |
+
f"Case: {meta.get('case_name','')}\n"
|
| 457 |
+
f"Citation: {meta.get('neutral_citation','')}\n"
|
| 458 |
+
f"Issue: {meta.get('issue','')}\n"
|
| 459 |
+
f"Summary: {meta.get('short_summary','')}\n"
|
| 460 |
+
f"Headnote: {meta.get('full_headnote','')[:1500]}\n"
|
| 461 |
+
f"Document: {text[:1500]}\n"
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
# =====================================================================
|
| 466 |
+
# UNIFIED RERANKING
|
| 467 |
+
# =====================================================================
|
| 468 |
+
|
| 469 |
+
def unified_rerank(query: str, evidence: list[Evidence]) -> list[Evidence]:
|
| 470 |
+
"""
|
| 471 |
+
Re-scores ALL evidence (statute + judgment) on the same cross-encoder
|
| 472 |
+
so they are directly comparable. This is what makes "best evidence
|
| 473 |
+
overall" meaningful rather than just concatenating two top-5 lists.
|
| 474 |
+
|
| 475 |
+
Direct/exact matches (score == 999.0) are pulled out, reranked
|
| 476 |
+
separately to preserve their relative order, then placed first.
|
| 477 |
+
Everything else is reranked together and capped by source-share
|
| 478 |
+
limits so neither source can completely starve the other.
|
| 479 |
+
"""
|
| 480 |
+
if not evidence:
|
| 481 |
+
return []
|
| 482 |
+
|
| 483 |
+
exact_hits = [e for e in evidence if e.score == 999.0]
|
| 484 |
+
semantic_ev = [e for e in evidence if e.score != 999.0]
|
| 485 |
+
|
| 486 |
+
# Rerank exact hits against each other (rare to have many, but
|
| 487 |
+
# if user asks about 2 sections + cites a case, order matters)
|
| 488 |
+
if len(exact_hits) > 1:
|
| 489 |
+
pairs = [(query, e.rerank_text) for e in exact_hits]
|
| 490 |
+
scores = stat_rag.reranker.predict(pairs)
|
| 491 |
+
for e, s in zip(exact_hits, scores):
|
| 492 |
+
e.unified_score = 1000.0 + float(s) # keep them above all semantic
|
| 493 |
+
exact_hits.sort(key=lambda e: e.unified_score, reverse=True)
|
| 494 |
+
else:
|
| 495 |
+
for e in exact_hits:
|
| 496 |
+
e.unified_score = 1000.0
|
| 497 |
+
|
| 498 |
+
# Rerank everything else together
|
| 499 |
+
if semantic_ev:
|
| 500 |
+
pairs = [(query, e.rerank_text) for e in semantic_ev]
|
| 501 |
+
scores = stat_rag.reranker.predict(pairs)
|
| 502 |
+
for e, s in zip(semantic_ev, scores):
|
| 503 |
+
e.unified_score = float(s)
|
| 504 |
+
semantic_ev.sort(key=lambda e: e.unified_score, reverse=True)
|
| 505 |
+
|
| 506 |
+
merged = exact_hits + semantic_ev
|
| 507 |
+
|
| 508 |
+
# Enforce source-share cap so one source can't drown out the other
|
| 509 |
+
# in the final cut, while still respecting overall rank order
|
| 510 |
+
final: list[Evidence] = []
|
| 511 |
+
type_counts = {"statute": 0, "judgment": 0}
|
| 512 |
+
seen_keys: set = set()
|
| 513 |
+
|
| 514 |
+
for e in merged:
|
| 515 |
+
if len(final) >= FINAL_EVIDENCE_N:
|
| 516 |
+
break
|
| 517 |
+
# Dedupe: same statute section (or same case citation) can arrive as both
|
| 518 |
+
# a direct/exact hit and a semantic hit — keep only the first (best) one.
|
| 519 |
+
if e.source_type == "statute":
|
| 520 |
+
dkey = ("s", str(e.meta.get("act_short", "")).upper(), str(e.meta.get("section_number", "")))
|
| 521 |
+
else:
|
| 522 |
+
dkey = ("j", str(e.meta.get("neutral_citation", "")).upper())
|
| 523 |
+
if dkey in seen_keys:
|
| 524 |
+
continue
|
| 525 |
+
if type_counts[e.source_type] >= MAX_SINGLE_TYPE_SHARE:
|
| 526 |
+
continue
|
| 527 |
+
seen_keys.add(dkey)
|
| 528 |
+
final.append(e)
|
| 529 |
+
type_counts[e.source_type] += 1
|
| 530 |
+
|
| 531 |
+
log.info(
|
| 532 |
+
"Unified rerank: %d total -> %d final (statute=%d, judgment=%d)",
|
| 533 |
+
len(merged), len(final), type_counts["statute"], type_counts["judgment"],
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
for i, e in enumerate(final, 1):
|
| 537 |
+
log.info(
|
| 538 |
+
" #%d [%s] score=%.3f %s",
|
| 539 |
+
i, e.source_type, e.unified_score,
|
| 540 |
+
(e.meta.get("title") or e.meta.get("case_name") or "")[:50],
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
return final
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
# =====================================================================
|
| 547 |
+
# CONTEXT ASSEMBLY
|
| 548 |
+
# =====================================================================
|
| 549 |
+
|
| 550 |
+
def build_unified_context(evidence: list[Evidence]) -> str:
|
| 551 |
+
return "\n".join(e.display_block for e in evidence)
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
# =====================================================================
|
| 555 |
+
# LLM GENERATION
|
| 556 |
+
# =====================================================================
|
| 557 |
+
|
| 558 |
+
PROMPTS = {
|
| 559 |
+
"LEGAL_RESEARCH": """\
|
| 560 |
+
You are an expert Indian Legal Assistant with deep knowledge of both \
|
| 561 |
+
statutory law (IPC, BNS, CrPC, BNSS, IEA, BSA) and Supreme Court \
|
| 562 |
+
jurisprudence interpreting that law.
|
| 563 |
+
|
| 564 |
+
EVIDENCE-GROUNDING RULES — THESE OVERRIDE YOUR GENERAL LEGAL KNOWLEDGE:
|
| 565 |
+
|
| 566 |
+
1. Every factual or legal claim you make must be traceable to a specific \
|
| 567 |
+
block in the EVIDENCE section below. Before writing any sentence that \
|
| 568 |
+
states what a court held, what a statute requires, or what factors a \
|
| 569 |
+
court considered, locate the exact evidence block that supports it.
|
| 570 |
+
|
| 571 |
+
2. You have broad general knowledge of Indian law from training. \
|
| 572 |
+
DO NOT use that general knowledge to fill gaps in the evidence, even if \
|
| 573 |
+
you are confident it is legally correct. If the evidence does not contain \
|
| 574 |
+
a point, omit the point — do not add it from memory. This applies even to \
|
| 575 |
+
well-known legal maxims (e.g. "bail is the rule, jail is the exception") \
|
| 576 |
+
unless that maxim is explicitly discussed in the evidence provided.
|
| 577 |
+
|
| 578 |
+
3. If you are tempted to write a claim and cannot point to which evidence \
|
| 579 |
+
block supports it, do not write it. Stop and either omit it or explicitly \
|
| 580 |
+
flag it as your own general knowledge using the marker \
|
| 581 |
+
"[Note: general principle, not found in retrieved evidence]" — use this \
|
| 582 |
+
marker sparingly and only when the point is necessary for a complete answer.
|
| 583 |
+
|
| 584 |
+
4. Do not blend evidence from a judgment with evidence from a statute (or \
|
| 585 |
+
vice versa) into a single unmarked sentence that implies one source said \
|
| 586 |
+
both things.
|
| 587 |
+
|
| 588 |
+
5. Only attribute a holding to a case if the holding text actually \
|
| 589 |
+
appears in that judgment's evidence block. Only cite a statute section \
|
| 590 |
+
if it appears in a statute evidence block.
|
| 591 |
+
|
| 592 |
+
6. If multiple evidence blocks are provided but only some are relevant to \
|
| 593 |
+
the question, silently drop the irrelevant ones — do not force them into \
|
| 594 |
+
the table or the answer.
|
| 595 |
+
|
| 596 |
+
7. If the evidence is insufficient to answer part of the question, say so \
|
| 597 |
+
explicitly rather than guessing.
|
| 598 |
+
|
| 599 |
+
OUTPUT FORMAT — THIS IS FOR DIRECT PRESENTATION, USE EXACTLY THIS SHAPE:
|
| 600 |
+
|
| 601 |
+
8. Start with a 1-2 sentence direct answer to the question. No heading.
|
| 602 |
+
|
| 603 |
+
9. Then give ONE markdown table, with these columns: Citation | Parties | \
|
| 604 |
+
Section/Issue | Holding.
|
| 605 |
+
- Citation: case name + neutral citation (e.g. "DDA v. Corporation \
|
| 606 |
+
Bank, 2025 INSC 1161"), or statute + section (e.g. "BNSS Sec. 480").
|
| 607 |
+
- Parties: who the dispute was between, in 3-5 words.
|
| 608 |
+
- Section/Issue: the specific statutory provision or legal question \
|
| 609 |
+
at stake, in one short phrase.
|
| 610 |
+
- Holding: what the court/statute actually decided or requires, in \
|
| 611 |
+
1 sentence, plain language.
|
| 612 |
+
Include one row per relevant case or statute provision in the \
|
| 613 |
+
evidence. Do not repeat the same case across multiple rows.
|
| 614 |
+
|
| 615 |
+
10. After the table, add at most 2-3 sentences of synthesis ONLY if \
|
| 616 |
+
needed to connect the rows to the user's specific question (e.g. how \
|
| 617 |
+
the precedent maps onto their fact pattern). If the table is self- \
|
| 618 |
+
explanatory, skip this entirely.
|
| 619 |
+
|
| 620 |
+
11. Do NOT use any other headings (no #, ##, ###), no "Summary" or \
|
| 621 |
+
"Conclusion" section, and no prose paragraphs walking through each case \
|
| 622 |
+
one by one outside the table. The table IS the structure — don't \
|
| 623 |
+
duplicate its content in prose above or below it.
|
| 624 |
+
|
| 625 |
+
12. If a legally material qualification doesn't fit in one table cell \
|
| 626 |
+
(e.g. a key exception), add it as a single short sentence after the \
|
| 627 |
+
table, not a new row or section.\
|
| 628 |
+
""",
|
| 629 |
+
|
| 630 |
+
"CASE_SUMMARY": """\
|
| 631 |
+
You are an expert Indian Legal Assistant. The user has asked for a summary of a specific case or statute.
|
| 632 |
+
|
| 633 |
+
EVIDENCE-GROUNDING RULES:
|
| 634 |
+
1. You must ONLY use the provided EVIDENCE to generate the summary. Do not invent facts or holdings.
|
| 635 |
+
2. If the case/statute requested is not in the EVIDENCE, state that you cannot provide a summary based on the retrieved documents.
|
| 636 |
+
|
| 637 |
+
OUTPUT FORMAT:
|
| 638 |
+
Provide a structured summary in Markdown using the following headings (omit any that are irrelevant or lack evidence):
|
| 639 |
+
**Facts**: Brief background of the case.
|
| 640 |
+
**Issues**: The core legal questions the court had to decide.
|
| 641 |
+
**Reasoning**: The court's rationale and legal analysis.
|
| 642 |
+
**Holding / Rule of Law**: The final decision or the legal principle established.
|
| 643 |
+
|
| 644 |
+
Do NOT include a citation table.\
|
| 645 |
+
""",
|
| 646 |
+
|
| 647 |
+
"CASE_COMPARISON": """\
|
| 648 |
+
You are an expert Indian Legal Assistant. The user has asked to compare multiple cases, statutes, or legal concepts.
|
| 649 |
+
|
| 650 |
+
EVIDENCE-GROUNDING RULES:
|
| 651 |
+
1. Every comparison point must be traceable to the EVIDENCE section below.
|
| 652 |
+
2. Do not hallucinate differences or similarities. Only use the retrieved text.
|
| 653 |
+
3. If the evidence does not provide enough information for a fair comparison, state the limitations explicitly.
|
| 654 |
+
|
| 655 |
+
OUTPUT FORMAT:
|
| 656 |
+
1. Start with a brief 1-2 sentence overview of the comparison.
|
| 657 |
+
2. Provide a Markdown table contrasting the entities. The columns should represent the entities (e.g., 'Section 302 IPC' | 'Section 103 BNS', or 'Case A' | 'Case B') and the rows should be the points of comparison (e.g., 'Punishment', 'Definition', 'Core Issue').
|
| 658 |
+
3. After the table, write a brief synthesis explaining the key differences or similarities based on the table.
|
| 659 |
+
Do NOT use the standard citation table format.\
|
| 660 |
+
""",
|
| 661 |
+
|
| 662 |
+
"COMPREHENSIVE_CASE_STUDY": """\
|
| 663 |
+
You are an expert Indian Legal Scholar. The user has requested a comprehensive, in-depth case study or detailed legal research report on a specific case or topic.
|
| 664 |
+
|
| 665 |
+
EVIDENCE-GROUNDING RULES:
|
| 666 |
+
1. You must ONLY use the provided EVIDENCE. Do not invent arguments, facts, or rulings.
|
| 667 |
+
2. If the evidence lacks certain details (like specific arguments of the appellant), omit that section rather than hallucinating.
|
| 668 |
+
|
| 669 |
+
OUTPUT FORMAT:
|
| 670 |
+
Provide an extensive, highly detailed academic case study in Markdown using the following headings:
|
| 671 |
+
**1. Background & Context**: The factual matrix and history leading up to the Supreme Court.
|
| 672 |
+
**2. Core Legal Issues**: A detailed breakdown of the exact questions of law the court had to decide.
|
| 673 |
+
**3. Arguments Advanced**: What the Appellant and Respondent argued (if available in evidence).
|
| 674 |
+
**4. Precedents & Statutes Relied Upon**: Key laws and past cases cited by the court.
|
| 675 |
+
**5. Court's Analysis & Rationale**: An in-depth explanation of the court's reasoning, logical steps, and interpretation of the law.
|
| 676 |
+
**6. Final Judgment & Holding**: The final verdict and rule of law established.
|
| 677 |
+
**7. Legal Implications**: How this ruling impacts the broader legal landscape based on the court's dicta.
|
| 678 |
+
|
| 679 |
+
Ensure the response is thorough, analytical, and highly detailed. Do NOT use a standard citation table.\
|
| 680 |
+
""",
|
| 681 |
+
|
| 682 |
+
"STORY_EVALUATION": """\
|
| 683 |
+
You are an expert Indian Legal Advisor. The user has provided a personal narrative or factual scenario. Your job is to analyze their situation using ONLY the provided EVIDENCE.
|
| 684 |
+
|
| 685 |
+
EVIDENCE-GROUNDING RULES:
|
| 686 |
+
1. You must base your legal analysis strictly on the retrieved EVIDENCE.
|
| 687 |
+
2. Do not invent applicable sections or case laws. If the evidence doesn't support a claim, do not make it.
|
| 688 |
+
|
| 689 |
+
OUTPUT FORMAT:
|
| 690 |
+
Provide a structured legal analysis in Markdown using the following headings:
|
| 691 |
+
**1. Summary of Facts**: A very brief (1-2 sentences) summary of the user's situation.
|
| 692 |
+
**2. Applicable Legal Provisions**: Identify the specific statutes and sections (e.g., from IPC, BNS, etc.) that could be framed based on the story. Explain exactly WHY they apply using the evidence.
|
| 693 |
+
**3. Relevant Case Law**: Cite relevant Supreme Court judgments from the evidence that have similar fact patterns or deal with the same legal issues. Explain how the courts ruled in those similar situations.
|
| 694 |
+
**4. Legal Assessment & Potential Outcomes**: Provide a practical legal assessment of the situation based strictly on the retrieved law and precedents.
|
| 695 |
+
|
| 696 |
+
Do NOT include the standard citation table.\
|
| 697 |
+
"""
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
def classify_intent(query: str) -> str:
|
| 702 |
+
"""Classify the user's query into one of five intents."""
|
| 703 |
+
prompt = """
|
| 704 |
+
Classify the user's legal query into EXACTLY ONE intent.
|
| 705 |
+
DEFAULT to LEGAL_RESEARCH unless the query CLEARLY matches a more specific intent below.
|
| 706 |
+
|
| 707 |
+
- LEGAL_RESEARCH: the default for almost everything — any general legal question, what the
|
| 708 |
+
law says, what courts have held on a topic, applicable provisions, punishments,
|
| 709 |
+
definitions, conditions, procedure, etc.
|
| 710 |
+
- CASE_SUMMARY: ONLY when the user explicitly asks to summarize ONE specific named case or
|
| 711 |
+
neutral citation (e.g. "summarize 2025 INSC 337", "give me a summary of X v. Y").
|
| 712 |
+
- COMPREHENSIVE_CASE_STUDY: ONLY when the user explicitly asks for an in-depth/detailed case
|
| 713 |
+
study, report, or comprehensive analysis of a specific case.
|
| 714 |
+
- CASE_COMPARISON: ONLY when the user asks to compare/contrast two or more cases, statutes,
|
| 715 |
+
or doctrines.
|
| 716 |
+
- STORY_EVALUATION: ONLY when the user narrates a personal or hypothetical factual scenario
|
| 717 |
+
and asks what law applies to it.
|
| 718 |
+
|
| 719 |
+
Output ONLY the exact intent name. No other text.
|
| 720 |
+
"""
|
| 721 |
+
try:
|
| 722 |
+
response = stat_rag.llm_client.chat.completions.create(
|
| 723 |
+
model=LLM_MODEL,
|
| 724 |
+
messages=[
|
| 725 |
+
{"role": "system", "content": prompt},
|
| 726 |
+
{"role": "user", "content": query}
|
| 727 |
+
],
|
| 728 |
+
temperature=0.0,
|
| 729 |
+
max_tokens=10
|
| 730 |
+
)
|
| 731 |
+
intent = response.choices[0].message.content.strip().upper()
|
| 732 |
+
if intent in ["CASE_SUMMARY", "CASE_COMPARISON", "COMPREHENSIVE_CASE_STUDY", "STORY_EVALUATION"]:
|
| 733 |
+
return intent
|
| 734 |
+
return "LEGAL_RESEARCH"
|
| 735 |
+
except Exception as e:
|
| 736 |
+
log.warning("Intent classification failed: %s. Defaulting to LEGAL_RESEARCH.", e)
|
| 737 |
+
return "LEGAL_RESEARCH"
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
def generate_answer(
|
| 741 |
+
query: str,
|
| 742 |
+
context: str,
|
| 743 |
+
intent: str,
|
| 744 |
+
conversation_history: Optional[list[dict]] = None,
|
| 745 |
+
) -> str:
|
| 746 |
+
system_prompt = PROMPTS.get(intent, PROMPTS["LEGAL_RESEARCH"])
|
| 747 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 748 |
+
|
| 749 |
+
if conversation_history:
|
| 750 |
+
messages.extend(conversation_history[-8:])
|
| 751 |
+
|
| 752 |
+
messages.append({
|
| 753 |
+
"role": "user",
|
| 754 |
+
"content": f"QUESTION:\n{query}\n\nEVIDENCE:\n{context}",
|
| 755 |
+
})
|
| 756 |
+
|
| 757 |
+
try:
|
| 758 |
+
t0 = time.time()
|
| 759 |
+
response = stat_rag.llm_client.chat.completions.create(
|
| 760 |
+
model=LLM_MODEL,
|
| 761 |
+
messages=messages,
|
| 762 |
+
temperature=LLM_TEMPERATURE,
|
| 763 |
+
max_tokens=LLM_MAX_TOKENS,
|
| 764 |
+
stream=True,
|
| 765 |
+
)
|
| 766 |
+
|
| 767 |
+
full_response = ""
|
| 768 |
+
first_chunk = True
|
| 769 |
+
for chunk in response:
|
| 770 |
+
delta = chunk.choices[0].delta
|
| 771 |
+
if delta.content:
|
| 772 |
+
if first_chunk:
|
| 773 |
+
log.info("First token in %.2fs", time.time() - t0)
|
| 774 |
+
first_chunk = False
|
| 775 |
+
print(delta.content, end="", flush=True)
|
| 776 |
+
full_response += delta.content
|
| 777 |
+
print()
|
| 778 |
+
|
| 779 |
+
log.info("Generation complete in %.2fs", time.time() - t0)
|
| 780 |
+
return full_response
|
| 781 |
+
|
| 782 |
+
except Exception as e:
|
| 783 |
+
log.error("DeepSeek API error: %s", e)
|
| 784 |
+
return f"Error generating answer: {e}"
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
# =====================================================================
|
| 788 |
+
# MAIN UNIFIED PIPELINE
|
| 789 |
+
# =====================================================================
|
| 790 |
+
|
| 791 |
+
def ask(query: str, conversation_history: Optional[list[dict]] = None, force_intent: str = "AUTO") -> dict:
|
| 792 |
+
"""
|
| 793 |
+
Full unified pipeline:
|
| 794 |
+
1. Route query (statute / judgment / hybrid)
|
| 795 |
+
2. Retrieve from chosen source(s) in their native pipelines
|
| 796 |
+
3. Normalise into Evidence objects
|
| 797 |
+
4. Unified rerank across both sources
|
| 798 |
+
5. Build combined context
|
| 799 |
+
6. Generate answer via DeepSeek
|
| 800 |
+
|
| 801 |
+
Returns a dict with routing decision, evidence summary, and answer.
|
| 802 |
+
"""
|
| 803 |
+
t_start = time.time()
|
| 804 |
+
|
| 805 |
+
decision = route_query(query)
|
| 806 |
+
log.info("Route: statute=%s judgment=%s (%s)",
|
| 807 |
+
decision.use_statute, decision.use_judgment, decision.reason)
|
| 808 |
+
|
| 809 |
+
all_evidence: list[Evidence] = []
|
| 810 |
+
|
| 811 |
+
if decision.use_statute:
|
| 812 |
+
all_evidence.extend(get_statute_evidence(query))
|
| 813 |
+
|
| 814 |
+
if decision.use_judgment:
|
| 815 |
+
all_evidence.extend(get_judgment_evidence(query))
|
| 816 |
+
|
| 817 |
+
if not all_evidence:
|
| 818 |
+
# Before giving up, check if the user asked for a specific citation
|
| 819 |
+
# that exists on Bharat Courts but isn't in our local database.
|
| 820 |
+
explicit_citations = judg_rag.extract_citations(query)
|
| 821 |
+
if explicit_citations:
|
| 822 |
+
log.info("No local evidence found for %s (live lookup handled in backend).", explicit_citations)
|
| 823 |
+
live_res = {}
|
| 824 |
+
|
| 825 |
+
for cit in explicit_citations:
|
| 826 |
+
res = live_res.get(cit, {})
|
| 827 |
+
if res.get("verified") is True:
|
| 828 |
+
title = res.get("matched_case_name") or "Unknown Case"
|
| 829 |
+
all_evidence.append(Evidence(
|
| 830 |
+
source_type="judgment",
|
| 831 |
+
score=999.0,
|
| 832 |
+
unified_score=999.0,
|
| 833 |
+
rerank_text="",
|
| 834 |
+
display_block=(
|
| 835 |
+
f"--- LIVE WEB SEARCH ---\n"
|
| 836 |
+
f"Citation: {cit}\n"
|
| 837 |
+
f"Title: {title}\n"
|
| 838 |
+
f"Note: This case exists on the Supreme Court website, but its full "
|
| 839 |
+
f"text is NOT downloaded in the local database. You cannot provide "
|
| 840 |
+
f"a deep legal analysis, but you MUST confirm to the user that it exists online."
|
| 841 |
+
),
|
| 842 |
+
meta={"neutral_citation": cit, "case_name": title}
|
| 843 |
+
))
|
| 844 |
+
log.info("Found %s on Bharat Courts. Injected as web-search evidence.", cit)
|
| 845 |
+
|
| 846 |
+
if not all_evidence:
|
| 847 |
+
return {
|
| 848 |
+
"query": query,
|
| 849 |
+
"route": decision.reason,
|
| 850 |
+
"evidence": [],
|
| 851 |
+
"answer": "No relevant statutes or judgments found for this query.",
|
| 852 |
+
"elapsed_seconds": round(time.time() - t_start, 2),
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
# Build the same severity-enriched query used inside get_judgment_evidence
|
| 856 |
+
# and reuse it here. Without this, judg_rag.retrieve() correctly ranks
|
| 857 |
+
# Darshan above Shuvendu Saha internally, but unified_rerank() would
|
| 858 |
+
# rescore everything from the BARE query and undo that improvement —
|
| 859 |
+
# the unified reranker has no other way to know "BNS 103" means murder.
|
| 860 |
+
severity_context = _build_severity_context(query)
|
| 861 |
+
unified_query = f"{query} {severity_context}" if severity_context else query
|
| 862 |
+
if severity_context:
|
| 863 |
+
log.info("Unified rerank query enriched with: '%s'", severity_context)
|
| 864 |
+
|
| 865 |
+
final_evidence = unified_rerank(unified_query, all_evidence)
|
| 866 |
+
context = build_unified_context(final_evidence)
|
| 867 |
+
|
| 868 |
+
if force_intent == "AUTO":
|
| 869 |
+
intent = classify_intent(query)
|
| 870 |
+
log.info("Detected Intent: %s", intent)
|
| 871 |
+
else:
|
| 872 |
+
intent = force_intent
|
| 873 |
+
log.info("Forced Intent by User: %s", intent)
|
| 874 |
+
|
| 875 |
+
print()
|
| 876 |
+
print("=" * 70)
|
| 877 |
+
print(f" UNIFIED LEGAL ANSWER ({intent})")
|
| 878 |
+
print("=" * 70)
|
| 879 |
+
print()
|
| 880 |
+
|
| 881 |
+
answer = generate_answer(query, context, intent, conversation_history)
|
| 882 |
+
|
| 883 |
+
# Verification is performed in the backend (live citation verifier), not here.
|
| 884 |
+
verification = {}
|
| 885 |
+
|
| 886 |
+
elapsed = time.time() - t_start
|
| 887 |
+
|
| 888 |
+
return {
|
| 889 |
+
"query": query,
|
| 890 |
+
"route": decision.reason,
|
| 891 |
+
"intent": intent,
|
| 892 |
+
"evidence": [
|
| 893 |
+
{
|
| 894 |
+
"type": e.source_type,
|
| 895 |
+
"score": round(e.unified_score, 3),
|
| 896 |
+
"label": e.meta.get("title") or e.meta.get("case_name") or "",
|
| 897 |
+
}
|
| 898 |
+
for e in final_evidence
|
| 899 |
+
],
|
| 900 |
+
"answer": answer,
|
| 901 |
+
"verification": verification,
|
| 902 |
+
"elapsed_seconds": round(elapsed, 2),
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
|
| 906 |
+
# =====================================================================
|
| 907 |
+
# CLI
|
| 908 |
+
# =====================================================================
|
| 909 |
+
|
| 910 |
+
def display_banner():
|
| 911 |
+
print()
|
| 912 |
+
print("=" * 60)
|
| 913 |
+
print(" LegalAIapex — Unified Statute + Judgment RAG")
|
| 914 |
+
print("=" * 60)
|
| 915 |
+
print(" Type a legal question, or 'exit' to quit.")
|
| 916 |
+
print("=" * 60)
|
| 917 |
+
print()
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
def main():
|
| 921 |
+
display_banner()
|
| 922 |
+
conversation_history: list[dict] = []
|
| 923 |
+
|
| 924 |
+
while True:
|
| 925 |
+
try:
|
| 926 |
+
query = input("Ask a legal question: ").strip()
|
| 927 |
+
except (EOFError, KeyboardInterrupt):
|
| 928 |
+
print("\nGoodbye.")
|
| 929 |
+
break
|
| 930 |
+
|
| 931 |
+
if not query:
|
| 932 |
+
continue
|
| 933 |
+
if query.lower() in ("exit", "quit", "q"):
|
| 934 |
+
print("Goodbye.")
|
| 935 |
+
break
|
| 936 |
+
if query.lower() == "clear":
|
| 937 |
+
conversation_history.clear()
|
| 938 |
+
print("Conversation history cleared.")
|
| 939 |
+
continue
|
| 940 |
+
|
| 941 |
+
result = ask(query, conversation_history)
|
| 942 |
+
|
| 943 |
+
conversation_history.append({"role": "user", "content": query})
|
| 944 |
+
conversation_history.append({"role": "assistant", "content": result["answer"]})
|
| 945 |
+
|
| 946 |
+
print()
|
| 947 |
+
print("-" * 60)
|
| 948 |
+
print(f"Route: {result['route']}")
|
| 949 |
+
print("Evidence used:")
|
| 950 |
+
for ev in result["evidence"]:
|
| 951 |
+
print(f" [{ev['type']:8s}] score={ev['score']:+.3f} {ev['label'][:50]}")
|
| 952 |
+
|
| 953 |
+
# --- Verification summary ---
|
| 954 |
+
v = result.get("verification", {})
|
| 955 |
+
if v:
|
| 956 |
+
grounded = v.get("grounded", None)
|
| 957 |
+
status = "✅ FULLY GROUNDED" if grounded else "⚠️ UNGROUNDED CITATIONS DETECTED"
|
| 958 |
+
print(f"\nVerification: {status}")
|
| 959 |
+
if v.get("flagged_sections"):
|
| 960 |
+
print(f" Unverified statute sections: {', '.join(v['flagged_sections'])}")
|
| 961 |
+
if v.get("citations_confirmed_via_live_lookup"):
|
| 962 |
+
print(f" Citations confirmed (live): {', '.join(v['citations_confirmed_via_live_lookup'])}")
|
| 963 |
+
if v.get("citations_likely_fabricated"):
|
| 964 |
+
print(f" ❌ Likely fabricated: {', '.join(v['citations_likely_fabricated'])}")
|
| 965 |
+
if v.get("citations_could_not_verify"):
|
| 966 |
+
print(f" ❓ Could not verify: {', '.join(v['citations_could_not_verify'])}")
|
| 967 |
+
|
| 968 |
+
print(f"Time: {result['elapsed_seconds']}s")
|
| 969 |
+
print("-" * 60)
|
| 970 |
+
|
| 971 |
+
|
| 972 |
+
if __name__ == "__main__":
|
| 973 |
+
main()
|
llm_retriever.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import chromadb
|
| 4 |
+
from sentence_transformers import SentenceTransformer, CrossEncoder
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
from rank_bm25 import BM25Okapi
|
| 7 |
+
|
| 8 |
+
# =====================================================
|
| 9 |
+
# CONFIG
|
| 10 |
+
# =====================================================
|
| 11 |
+
|
| 12 |
+
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
| 13 |
+
|
| 14 |
+
# Path to the SCI judgments Chroma index. In the container this is set to the
|
| 15 |
+
# location the dataset is downloaded to (see Dockerfile); locally it can point
|
| 16 |
+
# at a copy on disk. No more hardcoded Windows path.
|
| 17 |
+
CHROMA_PATH = os.getenv(
|
| 18 |
+
"JUDGMENTS_CHROMA_PATH",
|
| 19 |
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "chroma_bge_v2"),
|
| 20 |
+
)
|
| 21 |
+
COLLECTION_NAME = "sci_judgments_bge_v2"
|
| 22 |
+
|
| 23 |
+
# Retrieval tuning
|
| 24 |
+
DENSE_TOP_K = 150 # dense candidates before fusion
|
| 25 |
+
BM25_TOP_K = 150 # BM25 candidates before fusion
|
| 26 |
+
DEDUP_MAX_PER_CASE = 3 # max child chunks per case after dedup
|
| 27 |
+
RERANK_TOP_N = 30 # top-N children to rerank after fusion
|
| 28 |
+
PARENT_FETCH_N = 20 # how many parent docs to fetch
|
| 29 |
+
PARENT_FINAL_N = 5 # how many parents to pass to LLM
|
| 30 |
+
RRF_K = 60 # RRF constant (standard value)
|
| 31 |
+
|
| 32 |
+
# =====================================================
|
| 33 |
+
# LOAD CHROMA
|
| 34 |
+
# =====================================================
|
| 35 |
+
|
| 36 |
+
print("Connecting to ChromaDB...")
|
| 37 |
+
|
| 38 |
+
db_client = chromadb.PersistentClient(path=CHROMA_PATH)
|
| 39 |
+
collection = db_client.get_collection(COLLECTION_NAME)
|
| 40 |
+
|
| 41 |
+
print(f"Collection loaded: {collection.count()} chunks")
|
| 42 |
+
|
| 43 |
+
# =====================================================
|
| 44 |
+
# BUILD BM25 INDEX
|
| 45 |
+
# =====================================================
|
| 46 |
+
|
| 47 |
+
print("Loading full corpus for BM25...")
|
| 48 |
+
|
| 49 |
+
all_docs = collection.get(include=["documents", "metadatas"])
|
| 50 |
+
# Note: IDs are always returned automatically by ChromaDB — no need to include them
|
| 51 |
+
|
| 52 |
+
bm25_corpus = [doc.lower().split() for doc in all_docs["documents"]]
|
| 53 |
+
bm25_index = BM25Okapi(bm25_corpus)
|
| 54 |
+
|
| 55 |
+
print(f"BM25 indexed {len(bm25_corpus)} chunks")
|
| 56 |
+
|
| 57 |
+
# =====================================================
|
| 58 |
+
# LOAD EMBEDDING MODEL
|
| 59 |
+
# =====================================================
|
| 60 |
+
|
| 61 |
+
print("Loading embedding model...")
|
| 62 |
+
embedder = SentenceTransformer("BAAI/bge-small-en-v1.5")
|
| 63 |
+
|
| 64 |
+
# =====================================================
|
| 65 |
+
# LOAD RERANKER
|
| 66 |
+
# =====================================================
|
| 67 |
+
|
| 68 |
+
print("Loading reranker...")
|
| 69 |
+
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
|
| 70 |
+
# TODO: swap to Cohere Rerank v3 before production
|
| 71 |
+
|
| 72 |
+
# =====================================================
|
| 73 |
+
# LOAD DEEPSEEK
|
| 74 |
+
# =====================================================
|
| 75 |
+
|
| 76 |
+
llm = OpenAI(
|
| 77 |
+
api_key=DEEPSEEK_API_KEY,
|
| 78 |
+
base_url="https://api.deepseek.com"
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
print("Pipeline ready.\n")
|
| 82 |
+
|
| 83 |
+
# =====================================================
|
| 84 |
+
# QUERY EXPANSION
|
| 85 |
+
# =====================================================
|
| 86 |
+
|
| 87 |
+
EXPANSION_MAP = {
|
| 88 |
+
"settlement": """
|
| 89 |
+
compromise amicable settlement lok adalat
|
| 90 |
+
one time settlement quashing after settlement
|
| 91 |
+
compounding of offence
|
| 92 |
+
""",
|
| 93 |
+
"quash": """
|
| 94 |
+
section 482 crpc abuse of process inherent powers
|
| 95 |
+
criminal proceedings high court quashing
|
| 96 |
+
""",
|
| 97 |
+
"bail": """
|
| 98 |
+
anticipatory bail regular bail section 439 crpc
|
| 99 |
+
bail conditions personal liberty article 21
|
| 100 |
+
""",
|
| 101 |
+
"lease": """
|
| 102 |
+
agreement to lease lease deed lessee lessor
|
| 103 |
+
leasehold rights transfer of property act
|
| 104 |
+
nazul land unearned income
|
| 105 |
+
""",
|
| 106 |
+
"auction": """
|
| 107 |
+
liquidation proceedings winding up company court
|
| 108 |
+
official liquidator auction sale confirmed
|
| 109 |
+
as is where is basis
|
| 110 |
+
""",
|
| 111 |
+
"arbitration": """
|
| 112 |
+
section 11 arbitration conciliation act
|
| 113 |
+
appointment of arbitrator section 34 award
|
| 114 |
+
enforcement challenge
|
| 115 |
+
""",
|
| 116 |
+
"contempt": """
|
| 117 |
+
contempt of court wilful disobedience
|
| 118 |
+
section 2 contempt of courts act
|
| 119 |
+
civil contempt criminal contempt
|
| 120 |
+
""",
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
def expand_query(query: str) -> str:
|
| 124 |
+
q_lower = query.lower()
|
| 125 |
+
expansion = query
|
| 126 |
+
for keyword, extra_terms in EXPANSION_MAP.items():
|
| 127 |
+
if keyword in q_lower:
|
| 128 |
+
expansion += " " + extra_terms.strip()
|
| 129 |
+
return expansion
|
| 130 |
+
|
| 131 |
+
# =====================================================
|
| 132 |
+
# QUERY TYPE DETECTION
|
| 133 |
+
# =====================================================
|
| 134 |
+
|
| 135 |
+
COMPARISON_KEYWORDS = [
|
| 136 |
+
"compare", "difference", "distinguish",
|
| 137 |
+
"contrast", "similarity", "similarities",
|
| 138 |
+
"common principle", "approach", "versus", "vs"
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
def is_comparison_query(query: str) -> bool:
|
| 142 |
+
q = query.lower()
|
| 143 |
+
return any(k in q for k in COMPARISON_KEYWORDS)
|
| 144 |
+
|
| 145 |
+
def extract_citations(text: str) -> list[str]:
|
| 146 |
+
"""Extract all INSC citations from a string."""
|
| 147 |
+
return list(dict.fromkeys(
|
| 148 |
+
re.findall(r"\d{4}\s+INSC\s+\d+", text.upper())
|
| 149 |
+
))
|
| 150 |
+
|
| 151 |
+
# =====================================================
|
| 152 |
+
# DENSE SEARCH
|
| 153 |
+
# =====================================================
|
| 154 |
+
|
| 155 |
+
def dense_search(query: str, top_k: int = DENSE_TOP_K) -> dict:
|
| 156 |
+
embedding = embedder.encode(
|
| 157 |
+
query,
|
| 158 |
+
normalize_embeddings=True
|
| 159 |
+
).tolist()
|
| 160 |
+
|
| 161 |
+
results = collection.query(
|
| 162 |
+
query_embeddings=[embedding],
|
| 163 |
+
n_results=top_k,
|
| 164 |
+
include=["documents", "metadatas", "distances"]
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
print(f"\nDense search returned {len(results['documents'][0])} chunks")
|
| 168 |
+
return results
|
| 169 |
+
|
| 170 |
+
# =====================================================
|
| 171 |
+
# BM25 SEARCH
|
| 172 |
+
# =====================================================
|
| 173 |
+
|
| 174 |
+
def bm25_search(query: str, top_k: int = BM25_TOP_K) -> list[tuple[int, float]]:
|
| 175 |
+
scores = bm25_index.get_scores(query.lower().split())
|
| 176 |
+
ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
|
| 177 |
+
nonzero = [(idx, score) for idx, score in ranked[:top_k] if score > 0]
|
| 178 |
+
print(f"BM25 search returned {len(nonzero)} non-zero chunks")
|
| 179 |
+
return nonzero
|
| 180 |
+
|
| 181 |
+
# =====================================================
|
| 182 |
+
# RRF FUSION
|
| 183 |
+
# =====================================================
|
| 184 |
+
|
| 185 |
+
def rrf_fusion(
|
| 186 |
+
dense_results: dict,
|
| 187 |
+
bm25_ranked: list[tuple[int, float]],
|
| 188 |
+
k: int = RRF_K
|
| 189 |
+
) -> dict:
|
| 190 |
+
"""
|
| 191 |
+
Reciprocal Rank Fusion of dense vector results and BM25 results.
|
| 192 |
+
Returns a unified results dict sorted by fused RRF score.
|
| 193 |
+
"""
|
| 194 |
+
scores: dict[str, dict] = {}
|
| 195 |
+
|
| 196 |
+
# --- Dense contribution ---
|
| 197 |
+
for rank, (doc, meta, dist) in enumerate(zip(
|
| 198 |
+
dense_results["documents"][0],
|
| 199 |
+
dense_results["metadatas"][0],
|
| 200 |
+
dense_results["distances"][0]
|
| 201 |
+
)):
|
| 202 |
+
# Use first 80 chars of doc as part of key to handle same-citation chunks
|
| 203 |
+
chunk_key = meta.get("neutral_citation", "") + "|" + doc[:80]
|
| 204 |
+
if chunk_key not in scores:
|
| 205 |
+
scores[chunk_key] = {
|
| 206 |
+
"doc": doc, "meta": meta, "dist": dist, "score": 0.0
|
| 207 |
+
}
|
| 208 |
+
scores[chunk_key]["score"] += 1.0 / (k + rank + 1)
|
| 209 |
+
|
| 210 |
+
# --- BM25 contribution ---
|
| 211 |
+
for rank, (idx, bm25_score) in enumerate(bm25_ranked):
|
| 212 |
+
doc = all_docs["documents"][idx]
|
| 213 |
+
meta = all_docs["metadatas"][idx]
|
| 214 |
+
chunk_key = meta.get("neutral_citation", "") + "|" + doc[:80]
|
| 215 |
+
if chunk_key not in scores:
|
| 216 |
+
scores[chunk_key] = {
|
| 217 |
+
"doc": doc, "meta": meta, "dist": 0.0, "score": 0.0
|
| 218 |
+
}
|
| 219 |
+
scores[chunk_key]["score"] += 1.0 / (k + rank + 1)
|
| 220 |
+
|
| 221 |
+
# Sort by fused score descending
|
| 222 |
+
sorted_chunks = sorted(
|
| 223 |
+
scores.values(), key=lambda x: x["score"], reverse=True
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
print(f"RRF fusion produced {len(sorted_chunks)} unique chunks")
|
| 227 |
+
|
| 228 |
+
return {
|
| 229 |
+
"documents": [[c["doc"] for c in sorted_chunks]],
|
| 230 |
+
"metadatas": [[c["meta"] for c in sorted_chunks]],
|
| 231 |
+
"distances": [[c["dist"] for c in sorted_chunks]]
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
# =====================================================
|
| 235 |
+
# DEDUPLICATION (max N child chunks per case)
|
| 236 |
+
# =====================================================
|
| 237 |
+
|
| 238 |
+
def deduplicate_children(results: dict, max_per_case: int = DEDUP_MAX_PER_CASE) -> dict:
|
| 239 |
+
seen: dict[str, int] = {}
|
| 240 |
+
docs, metas, dists = [], [], []
|
| 241 |
+
|
| 242 |
+
for doc, meta, dist in zip(
|
| 243 |
+
results["documents"][0],
|
| 244 |
+
results["metadatas"][0],
|
| 245 |
+
results["distances"][0]
|
| 246 |
+
):
|
| 247 |
+
citation = meta.get("neutral_citation", "")
|
| 248 |
+
count = seen.get(citation, 0)
|
| 249 |
+
if count >= max_per_case:
|
| 250 |
+
continue
|
| 251 |
+
seen[citation] = count + 1
|
| 252 |
+
docs.append(doc)
|
| 253 |
+
metas.append(meta)
|
| 254 |
+
dists.append(dist)
|
| 255 |
+
|
| 256 |
+
print(f"After dedup: {len(docs)} child chunks across {len(seen)} cases")
|
| 257 |
+
return {"documents": [docs], "metadatas": [metas], "distances": [dists]}
|
| 258 |
+
|
| 259 |
+
# =====================================================
|
| 260 |
+
# CHILD RERANKING
|
| 261 |
+
# =====================================================
|
| 262 |
+
|
| 263 |
+
def rerank_children(query: str, results: dict, top_n: int = RERANK_TOP_N) -> dict:
|
| 264 |
+
docs = results["documents"][0]
|
| 265 |
+
metas = results["metadatas"][0]
|
| 266 |
+
dists = results["distances"][0]
|
| 267 |
+
|
| 268 |
+
if not docs:
|
| 269 |
+
return results
|
| 270 |
+
|
| 271 |
+
pairs = [(query, doc) for doc in docs]
|
| 272 |
+
scores = reranker.predict(pairs)
|
| 273 |
+
|
| 274 |
+
combined = sorted(
|
| 275 |
+
zip(scores, docs, metas, dists),
|
| 276 |
+
key=lambda x: x[0],
|
| 277 |
+
reverse=True
|
| 278 |
+
)[:top_n]
|
| 279 |
+
|
| 280 |
+
print(f"Child reranking kept top {len(combined)} chunks")
|
| 281 |
+
|
| 282 |
+
return {
|
| 283 |
+
"documents": [[x[1] for x in combined]],
|
| 284 |
+
"metadatas": [[x[2] for x in combined]],
|
| 285 |
+
"distances": [[x[3] for x in combined]]
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
# =====================================================
|
| 289 |
+
# CASE RANKING FROM CHILDREN
|
| 290 |
+
# =====================================================
|
| 291 |
+
|
| 292 |
+
def rank_cases_from_children(child_results: dict) -> list[str]:
|
| 293 |
+
"""
|
| 294 |
+
Score each case by the sum of positional scores of its children.
|
| 295 |
+
Higher-ranked children contribute more to their case's score.
|
| 296 |
+
"""
|
| 297 |
+
case_scores: dict[str, float] = {}
|
| 298 |
+
|
| 299 |
+
for rank, meta in enumerate(child_results["metadatas"][0]):
|
| 300 |
+
citation = meta.get("neutral_citation", "")
|
| 301 |
+
if not citation:
|
| 302 |
+
continue
|
| 303 |
+
# Positional score: rank 0 = highest
|
| 304 |
+
case_scores[citation] = (
|
| 305 |
+
case_scores.get(citation, 0.0) + 1.0 / (rank + 1)
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
ranked = sorted(case_scores.items(), key=lambda x: x[1], reverse=True)
|
| 309 |
+
return [citation for citation, _ in ranked]
|
| 310 |
+
|
| 311 |
+
# =====================================================
|
| 312 |
+
# CITATION SEARCH (exact case lookup)
|
| 313 |
+
# =====================================================
|
| 314 |
+
|
| 315 |
+
def citation_search(citation: str) -> dict | None:
|
| 316 |
+
results = collection.get(where={"neutral_citation": citation})
|
| 317 |
+
|
| 318 |
+
for i, chunk_id in enumerate(results["ids"]):
|
| 319 |
+
if chunk_id.endswith("__parent"):
|
| 320 |
+
return {
|
| 321 |
+
"type": "citation",
|
| 322 |
+
"id": results["ids"][i],
|
| 323 |
+
"document": results["documents"][i],
|
| 324 |
+
"metadata": results["metadatas"][i]
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
# Fallback: return first chunk if no __parent marker
|
| 328 |
+
if results["ids"]:
|
| 329 |
+
return {
|
| 330 |
+
"type": "citation",
|
| 331 |
+
"id": results["ids"][0],
|
| 332 |
+
"document": results["documents"][0],
|
| 333 |
+
"metadata": results["metadatas"][0]
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
return None
|
| 337 |
+
|
| 338 |
+
# =====================================================
|
| 339 |
+
# PARENT FETCH
|
| 340 |
+
# =====================================================
|
| 341 |
+
|
| 342 |
+
def fetch_parent_chunks(citations: list[str]) -> list[dict]:
|
| 343 |
+
parents = []
|
| 344 |
+
|
| 345 |
+
for citation in citations:
|
| 346 |
+
data = collection.get(where={"neutral_citation": citation})
|
| 347 |
+
|
| 348 |
+
parent_found = False
|
| 349 |
+
for i, chunk_id in enumerate(data["ids"]):
|
| 350 |
+
if chunk_id.endswith("__parent"):
|
| 351 |
+
parents.append({
|
| 352 |
+
"id": data["ids"][i],
|
| 353 |
+
"document": data["documents"][i],
|
| 354 |
+
"metadata": data["metadatas"][i]
|
| 355 |
+
})
|
| 356 |
+
parent_found = True
|
| 357 |
+
break
|
| 358 |
+
|
| 359 |
+
# Fallback: use first chunk if no __parent
|
| 360 |
+
if not parent_found and data["ids"]:
|
| 361 |
+
parents.append({
|
| 362 |
+
"id": data["ids"][0],
|
| 363 |
+
"document": data["documents"][0],
|
| 364 |
+
"metadata": data["metadatas"][0]
|
| 365 |
+
})
|
| 366 |
+
|
| 367 |
+
print(f"Fetched {len(parents)} parent documents")
|
| 368 |
+
return parents
|
| 369 |
+
|
| 370 |
+
# =====================================================
|
| 371 |
+
# PARENT RERANKING
|
| 372 |
+
# =====================================================
|
| 373 |
+
|
| 374 |
+
def rerank_parents(query: str, parents: list[dict], top_n: int = PARENT_FINAL_N) -> list[dict]:
|
| 375 |
+
if not parents:
|
| 376 |
+
return parents
|
| 377 |
+
|
| 378 |
+
# Rerank on headnote + summary + document text combined
|
| 379 |
+
texts = []
|
| 380 |
+
for p in parents:
|
| 381 |
+
meta = p["metadata"]
|
| 382 |
+
combined_text = " ".join(filter(None, [
|
| 383 |
+
meta.get("full_headnote", "")[:2000],
|
| 384 |
+
meta.get("short_summary", ""),
|
| 385 |
+
meta.get("issue", ""),
|
| 386 |
+
p["document"][:1000]
|
| 387 |
+
]))
|
| 388 |
+
texts.append(combined_text)
|
| 389 |
+
|
| 390 |
+
pairs = [(query, t) for t in texts]
|
| 391 |
+
scores = reranker.predict(pairs)
|
| 392 |
+
|
| 393 |
+
ranked = sorted(
|
| 394 |
+
zip(scores, parents),
|
| 395 |
+
key=lambda x: x[0],
|
| 396 |
+
reverse=True
|
| 397 |
+
)[:top_n]
|
| 398 |
+
|
| 399 |
+
print(f"Parent reranking kept top {len(ranked)} parents")
|
| 400 |
+
return [p for _, p in ranked]
|
| 401 |
+
|
| 402 |
+
# =====================================================
|
| 403 |
+
# MAIN RETRIEVER
|
| 404 |
+
# =====================================================
|
| 405 |
+
|
| 406 |
+
def retrieve(query: str) -> dict | None:
|
| 407 |
+
query_expanded = expand_query(query)
|
| 408 |
+
citations = extract_citations(query)
|
| 409 |
+
|
| 410 |
+
# --------------------------------------------------
|
| 411 |
+
# MODE 1: Comparison — two or more explicit citations
|
| 412 |
+
# --------------------------------------------------
|
| 413 |
+
if len(citations) >= 2 and is_comparison_query(query):
|
| 414 |
+
print("\nMODE: COMPARISON SEARCH")
|
| 415 |
+
parents = [r for c in citations if (r := citation_search(c))]
|
| 416 |
+
return {"type": "comparison", "parents": parents}
|
| 417 |
+
|
| 418 |
+
# --------------------------------------------------
|
| 419 |
+
# MODE 2: Single explicit citation lookup
|
| 420 |
+
# --------------------------------------------------
|
| 421 |
+
if len(citations) == 1:
|
| 422 |
+
print("\nMODE: CITATION SEARCH")
|
| 423 |
+
return citation_search(citations[0])
|
| 424 |
+
|
| 425 |
+
# --------------------------------------------------
|
| 426 |
+
# MODE 3: Hybrid semantic search
|
| 427 |
+
# --------------------------------------------------
|
| 428 |
+
print("\nMODE: HYBRID SEARCH (dense + BM25 + RRF + rerank)")
|
| 429 |
+
|
| 430 |
+
# Step 1: Dense + BM25 retrieval
|
| 431 |
+
dense_results = dense_search(query_expanded, top_k=DENSE_TOP_K)
|
| 432 |
+
bm25_ranked = bm25_search(query_expanded, top_k=BM25_TOP_K)
|
| 433 |
+
|
| 434 |
+
# Step 2: RRF fusion
|
| 435 |
+
fused = rrf_fusion(dense_results, bm25_ranked)
|
| 436 |
+
|
| 437 |
+
# Step 3: Deduplicate (max 3 child chunks per case)
|
| 438 |
+
fused = deduplicate_children(fused)
|
| 439 |
+
|
| 440 |
+
# Step 4: Rerank fused children
|
| 441 |
+
fused = rerank_children(query, fused, top_n=RERANK_TOP_N)
|
| 442 |
+
|
| 443 |
+
# Step 5: Log top-10 children after reranking
|
| 444 |
+
print("\nTOP 10 CHILDREN AFTER RERANKING:")
|
| 445 |
+
for i, meta in enumerate(fused["metadatas"][0][:10]):
|
| 446 |
+
print(f" {i+1}. {meta.get('neutral_citation','')} | {meta.get('case_name','')}")
|
| 447 |
+
|
| 448 |
+
# Step 6: Rank cases by child scores
|
| 449 |
+
ranked_citations = rank_cases_from_children(fused)
|
| 450 |
+
|
| 451 |
+
print("\nCASE SCORES (top 10):")
|
| 452 |
+
for c in ranked_citations[:10]:
|
| 453 |
+
print(f" {c}")
|
| 454 |
+
|
| 455 |
+
# Step 7: Fetch parent documents
|
| 456 |
+
parents = fetch_parent_chunks(ranked_citations[:PARENT_FETCH_N])
|
| 457 |
+
if not parents:
|
| 458 |
+
print("No parent documents found.")
|
| 459 |
+
return None
|
| 460 |
+
|
| 461 |
+
# Step 8: Rerank parents directly against original query
|
| 462 |
+
parents = rerank_parents(query, parents, top_n=PARENT_FINAL_N)
|
| 463 |
+
|
| 464 |
+
print("\nFINAL RETRIEVED CASES:")
|
| 465 |
+
for p in parents:
|
| 466 |
+
meta = p["metadata"]
|
| 467 |
+
print(f" {meta.get('neutral_citation','')} | {meta.get('case_name','')}")
|
| 468 |
+
|
| 469 |
+
return {"type": "semantic", "parents": parents}
|
| 470 |
+
|
| 471 |
+
# =====================================================
|
| 472 |
+
# CONTEXT BUILDER
|
| 473 |
+
# =====================================================
|
| 474 |
+
|
| 475 |
+
def build_context(result: dict) -> str:
|
| 476 |
+
if result is None:
|
| 477 |
+
return ""
|
| 478 |
+
|
| 479 |
+
rtype = result.get("type", "")
|
| 480 |
+
|
| 481 |
+
# Single citation lookup
|
| 482 |
+
if rtype in ("citation", "case"):
|
| 483 |
+
meta = result["metadata"]
|
| 484 |
+
return f"""
|
| 485 |
+
CASE NAME: {meta.get('case_name', '')}
|
| 486 |
+
CITATION: {meta.get('neutral_citation', '')}
|
| 487 |
+
ISSUE: {meta.get('issue', '')}
|
| 488 |
+
SUMMARY: {meta.get('short_summary', '')}
|
| 489 |
+
DOCUMENT:
|
| 490 |
+
{result['document']}
|
| 491 |
+
"""
|
| 492 |
+
|
| 493 |
+
# Comparison or semantic — multiple parents
|
| 494 |
+
if rtype in ("comparison", "semantic"):
|
| 495 |
+
blocks = []
|
| 496 |
+
for case in result["parents"]:
|
| 497 |
+
meta = case["metadata"]
|
| 498 |
+
blocks.append(f"""
|
| 499 |
+
{'='*60}
|
| 500 |
+
CASE NAME: {meta.get('case_name', '')}
|
| 501 |
+
CITATION: {meta.get('neutral_citation', '')}
|
| 502 |
+
ISSUE: {meta.get('issue', '')}
|
| 503 |
+
SUMMARY: {meta.get('short_summary', '')}
|
| 504 |
+
HEADNOTE:
|
| 505 |
+
{meta.get('full_headnote', '')[:5000]}
|
| 506 |
+
DOCUMENT:
|
| 507 |
+
{case['document'][:3000]}
|
| 508 |
+
{'='*60}
|
| 509 |
+
""")
|
| 510 |
+
return "\n".join(blocks)
|
| 511 |
+
|
| 512 |
+
return ""
|
| 513 |
+
|
| 514 |
+
# =====================================================
|
| 515 |
+
# LLM ANSWER GENERATION
|
| 516 |
+
# =====================================================
|
| 517 |
+
|
| 518 |
+
SYSTEM_PROMPT = """You are an expert Indian legal research assistant with deep knowledge
|
| 519 |
+
of Supreme Court jurisprudence. Answer questions accurately using only the supplied
|
| 520 |
+
legal material. Always cite the case name and neutral citation."""
|
| 521 |
+
|
| 522 |
+
RESEARCH_PROMPT = """
|
| 523 |
+
You are an expert Indian legal research assistant.
|
| 524 |
+
|
| 525 |
+
Answer the USER QUESTION using ONLY the supplied legal material.
|
| 526 |
+
|
| 527 |
+
Rules:
|
| 528 |
+
1. Answer the question directly and precisely.
|
| 529 |
+
2. State the legal principle clearly.
|
| 530 |
+
3. Cite the relevant case name and neutral citation (e.g. 2025 INSC 337).
|
| 531 |
+
4. Reference specific paragraph numbers where relevant.
|
| 532 |
+
5. Do not summarize the entire judgment — focus on the user's question.
|
| 533 |
+
6. Do not invent facts, principles, or citations.
|
| 534 |
+
7. If the answer is not in the supplied material, say so clearly.
|
| 535 |
+
|
| 536 |
+
LEGAL MATERIAL:
|
| 537 |
+
{context}
|
| 538 |
+
|
| 539 |
+
USER QUESTION:
|
| 540 |
+
{query}
|
| 541 |
+
"""
|
| 542 |
+
|
| 543 |
+
COMPARISON_PROMPT = """
|
| 544 |
+
You are an expert Indian legal research assistant.
|
| 545 |
+
|
| 546 |
+
Compare the supplied cases on the USER QUESTION.
|
| 547 |
+
|
| 548 |
+
For each case identify:
|
| 549 |
+
1. Material facts relevant to the question
|
| 550 |
+
2. Legal issue decided
|
| 551 |
+
3. Holding / ratio decidendi
|
| 552 |
+
4. Key paragraphs / observations
|
| 553 |
+
|
| 554 |
+
Then provide:
|
| 555 |
+
5. Similarities between the cases
|
| 556 |
+
6. Key differences / distinctions
|
| 557 |
+
7. Evolution or development of the legal principle
|
| 558 |
+
8. Practical rule a lawyer should apply
|
| 559 |
+
|
| 560 |
+
Use a structured format. Use tables where helpful.
|
| 561 |
+
Use ONLY the supplied material. Cite case names and citations throughout.
|
| 562 |
+
|
| 563 |
+
LEGAL MATERIAL:
|
| 564 |
+
{context}
|
| 565 |
+
|
| 566 |
+
USER QUESTION:
|
| 567 |
+
{query}
|
| 568 |
+
"""
|
| 569 |
+
|
| 570 |
+
def generate_answer(query: str, context: str, comparison: bool = False) -> str:
|
| 571 |
+
prompt_template = COMPARISON_PROMPT if comparison else RESEARCH_PROMPT
|
| 572 |
+
prompt = prompt_template.format(context=context, query=query)
|
| 573 |
+
|
| 574 |
+
response = llm.chat.completions.create(
|
| 575 |
+
model="deepseek-chat",
|
| 576 |
+
messages=[
|
| 577 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 578 |
+
{"role": "user", "content": prompt}
|
| 579 |
+
],
|
| 580 |
+
temperature=0,
|
| 581 |
+
max_tokens=2000
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
return response.choices[0].message.content
|
| 585 |
+
|
| 586 |
+
# =====================================================
|
| 587 |
+
# MAIN LOOP
|
| 588 |
+
# =====================================================
|
| 589 |
+
|
| 590 |
+
def main():
|
| 591 |
+
print("\n" + "="*60)
|
| 592 |
+
print(" LEGAL AI — SCI JUDGMENT RETRIEVAL SYSTEM")
|
| 593 |
+
print("="*60)
|
| 594 |
+
print("Type a legal question, a citation (e.g. 2025 INSC 337),")
|
| 595 |
+
print("or 'exit' to quit.\n")
|
| 596 |
+
|
| 597 |
+
while True:
|
| 598 |
+
query = input("Ask a legal question: ").strip()
|
| 599 |
+
|
| 600 |
+
if not query:
|
| 601 |
+
continue
|
| 602 |
+
|
| 603 |
+
if query.lower() == "exit":
|
| 604 |
+
print("Goodbye.")
|
| 605 |
+
break
|
| 606 |
+
|
| 607 |
+
# Retrieve
|
| 608 |
+
retrieved = retrieve(query)
|
| 609 |
+
|
| 610 |
+
if retrieved is None:
|
| 611 |
+
print("\nNo relevant cases found.")
|
| 612 |
+
continue
|
| 613 |
+
|
| 614 |
+
# Print retrieved cases summary
|
| 615 |
+
rtype = retrieved.get("type", "")
|
| 616 |
+
|
| 617 |
+
if rtype == "comparison":
|
| 618 |
+
print("\nCOMPARISON CASES:")
|
| 619 |
+
for case in retrieved["parents"]:
|
| 620 |
+
meta = case["metadata"]
|
| 621 |
+
print(f" {meta.get('neutral_citation','')} | {meta.get('case_name','')}")
|
| 622 |
+
|
| 623 |
+
elif rtype == "semantic":
|
| 624 |
+
print("\nRETRIEVED CASES:")
|
| 625 |
+
for parent in retrieved["parents"]:
|
| 626 |
+
meta = parent["metadata"]
|
| 627 |
+
print(f" {meta.get('neutral_citation','')} | {meta.get('case_name','')}")
|
| 628 |
+
|
| 629 |
+
# Build context and generate answer
|
| 630 |
+
context = build_context(retrieved)
|
| 631 |
+
comparison = (
|
| 632 |
+
rtype == "comparison"
|
| 633 |
+
or is_comparison_query(query)
|
| 634 |
+
)
|
| 635 |
+
answer = generate_answer(query, context, comparison=comparison)
|
| 636 |
+
|
| 637 |
+
print("\n" + "="*80)
|
| 638 |
+
print("ANSWER")
|
| 639 |
+
print("="*80)
|
| 640 |
+
print(answer)
|
| 641 |
+
print()
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
if __name__ == "__main__":
|
| 645 |
+
main()
|
metadata_matching_bharatlibrary.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from bharat_courts import JudgmentSearchClient
|
| 5 |
+
from bharat_courts.captcha.ocr import OCRCaptchaSolver
|
| 6 |
+
|
| 7 |
+
async def main():
|
| 8 |
+
jsonl_path = Path("data/html/extracted_judgments.jsonl")
|
| 9 |
+
pdf_dir = Path("data/pdfs")
|
| 10 |
+
state_path = Path("data/pdf_download_state.txt")
|
| 11 |
+
failed_path = Path("data/pdf_failed.jsonl")
|
| 12 |
+
pdf_dir.mkdir(parents=True, exist_ok=True)
|
| 13 |
+
|
| 14 |
+
# Load all records
|
| 15 |
+
records = []
|
| 16 |
+
with open(jsonl_path, "r", encoding="utf-8") as f:
|
| 17 |
+
for line in f:
|
| 18 |
+
if line.strip():
|
| 19 |
+
records.append(json.loads(line))
|
| 20 |
+
|
| 21 |
+
print(f"Loaded {len(records)} records")
|
| 22 |
+
|
| 23 |
+
# Resume — skip already downloaded
|
| 24 |
+
completed = set()
|
| 25 |
+
if state_path.exists():
|
| 26 |
+
completed = {line.strip() for line in state_path.read_text().splitlines() if line.strip()}
|
| 27 |
+
print(f"Already downloaded: {len(completed)} | Remaining: {len(records) - len(completed)}")
|
| 28 |
+
|
| 29 |
+
success, failed = 0, 0
|
| 30 |
+
|
| 31 |
+
async with JudgmentSearchClient(captcha_solver=OCRCaptchaSolver()) as client:
|
| 32 |
+
for i, record in enumerate(records, 1):
|
| 33 |
+
query = record.get("neutral_citation", "").strip()
|
| 34 |
+
if not query:
|
| 35 |
+
query = record.get("case_name", "").strip()
|
| 36 |
+
|
| 37 |
+
# Skip already downloaded
|
| 38 |
+
if query in completed:
|
| 39 |
+
print(f"[{i}/{len(records)}] Skipping: {query}")
|
| 40 |
+
continue
|
| 41 |
+
|
| 42 |
+
print(f"\n[{i}/{len(records)}] Searching: {query}")
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
results = await client.search(query, court_type="3", page_size=5)
|
| 46 |
+
|
| 47 |
+
if not results.items:
|
| 48 |
+
print(f" ✗ No results found")
|
| 49 |
+
failed += 1
|
| 50 |
+
with open(failed_path, "a") as ef:
|
| 51 |
+
ef.write(json.dumps({"query": query, "reason": "no_results"}) + "\n")
|
| 52 |
+
continue
|
| 53 |
+
|
| 54 |
+
judgment = results.items[0]
|
| 55 |
+
await client.download_pdf(judgment, court_type="3")
|
| 56 |
+
|
| 57 |
+
if judgment.pdf_bytes:
|
| 58 |
+
safe_name = query.replace(" ", "_").replace("/", "-")
|
| 59 |
+
pdf_path = pdf_dir / f"{safe_name}.pdf"
|
| 60 |
+
pdf_path.write_bytes(judgment.pdf_bytes)
|
| 61 |
+
record["pdf_path"] = str(pdf_path)
|
| 62 |
+
# Save progress
|
| 63 |
+
with open(state_path, "a") as sf:
|
| 64 |
+
sf.write(query + "\n")
|
| 65 |
+
completed.add(query)
|
| 66 |
+
print(f" ✓ {pdf_path.name} ({len(judgment.pdf_bytes) // 1024} KB)")
|
| 67 |
+
success += 1
|
| 68 |
+
else:
|
| 69 |
+
print(f" ✗ PDF bytes empty")
|
| 70 |
+
failed += 1
|
| 71 |
+
with open(failed_path, "a") as ef:
|
| 72 |
+
ef.write(json.dumps({"query": query, "reason": "empty_pdf"}) + "\n")
|
| 73 |
+
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f" ✗ Error: {e}")
|
| 76 |
+
failed += 1
|
| 77 |
+
with open(failed_path, "a") as ef:
|
| 78 |
+
ef.write(json.dumps({"query": query, "reason": str(e)}) + "\n")
|
| 79 |
+
|
| 80 |
+
# Save updated metadata with pdf_path
|
| 81 |
+
with open(jsonl_path, "w", encoding="utf-8") as f:
|
| 82 |
+
for record in records:
|
| 83 |
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 84 |
+
|
| 85 |
+
print(f"\n{'='*50}")
|
| 86 |
+
print(f"✓ Downloaded: {success} | ✗ Failed: {failed}")
|
| 87 |
+
|
| 88 |
+
asyncio.run(main())
|
metadata_retrieval.py
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SCR (Supreme Court Reports) Judgment Scraper
|
| 3 |
+
scraper with complete metadata extraction for RAG pipelines.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import requests
|
| 9 |
+
from bs4 import BeautifulSoup
|
| 10 |
+
import json
|
| 11 |
+
import re
|
| 12 |
+
import logging
|
| 13 |
+
import random
|
| 14 |
+
import time
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from dataclasses import dataclass, field, asdict
|
| 18 |
+
from typing import Optional
|
| 19 |
+
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
# Logging
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
Path("data").mkdir(exist_ok=True)
|
| 24 |
+
logging.basicConfig(
|
| 25 |
+
level=logging.INFO,
|
| 26 |
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
| 27 |
+
handlers=[
|
| 28 |
+
logging.StreamHandler(),
|
| 29 |
+
logging.FileHandler("data/scraper.log", encoding="utf-8"),
|
| 30 |
+
],
|
| 31 |
+
)
|
| 32 |
+
log = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
# Data model
|
| 37 |
+
# ---------------------------------------------------------------------------
|
| 38 |
+
@dataclass
|
| 39 |
+
class CaseCited:
|
| 40 |
+
name: str
|
| 41 |
+
citation: str
|
| 42 |
+
treatment: str # "relied on" | "referred to" | "overruled" | "distinguished"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class SectionRef:
|
| 47 |
+
act: str
|
| 48 |
+
provision: str # e.g. "s.61(2)", "r.22"
|
| 49 |
+
number: str # e.g. "61(2)", "22"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class JudgmentMetadata:
|
| 54 |
+
# --- identifiers ---
|
| 55 |
+
case_name: str = ""
|
| 56 |
+
appeal_no: str = ""
|
| 57 |
+
citation: str = ""
|
| 58 |
+
neutral_citation: str = ""
|
| 59 |
+
|
| 60 |
+
# --- court info ---
|
| 61 |
+
court: str = "Supreme Court"
|
| 62 |
+
lower_court: str = ""
|
| 63 |
+
jurisdiction: str = "India"
|
| 64 |
+
state: Optional[str] = None
|
| 65 |
+
|
| 66 |
+
# --- date ---
|
| 67 |
+
date: Optional[str] = None # ISO-8601 YYYY-MM-DD
|
| 68 |
+
|
| 69 |
+
# --- bench ---
|
| 70 |
+
bench: list = field(default_factory=list) # ["Sanjay Kumar, J", ...]
|
| 71 |
+
author_judge: str = ""
|
| 72 |
+
|
| 73 |
+
# --- outcome ---
|
| 74 |
+
outcome: str = ""
|
| 75 |
+
case_type: str = ""
|
| 76 |
+
|
| 77 |
+
# --- statutes ---
|
| 78 |
+
acts: list = field(default_factory=list)
|
| 79 |
+
sections: list = field(default_factory=list)
|
| 80 |
+
|
| 81 |
+
# --- case law ---
|
| 82 |
+
cases_cited: list = field(default_factory=list)
|
| 83 |
+
|
| 84 |
+
# --- text fields ---
|
| 85 |
+
keywords: list = field(default_factory=list)
|
| 86 |
+
issue: str = ""
|
| 87 |
+
short_summary: str = "" # headnote-derived, for metadata filtering
|
| 88 |
+
full_headnote: str = "" # full headnote text, used as RAG chunk content
|
| 89 |
+
|
| 90 |
+
# --- source ---
|
| 91 |
+
source_url: str = ""
|
| 92 |
+
scraped_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
| 93 |
+
pdf_path: str = ""
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# ---------------------------------------------------------------------------
|
| 97 |
+
# Constants
|
| 98 |
+
# ---------------------------------------------------------------------------
|
| 99 |
+
MONTHS = {
|
| 100 |
+
"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6,
|
| 101 |
+
"jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12,
|
| 102 |
+
"january": 1, "february": 2, "march": 3, "april": 4, "june": 6,
|
| 103 |
+
"july": 7, "august": 8, "september": 9, "october": 10, "november": 11,
|
| 104 |
+
"december": 12,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
# Canonical act names (keyed by lowercase fragment).
|
| 108 |
+
# All variants of the same act must map to the SAME canonical string —
|
| 109 |
+
# this prevents duplicate section entries under different name spellings.
|
| 110 |
+
ACT_ALIASES = {
|
| 111 |
+
"insolvency and bankruptcy code": "Insolvency and Bankruptcy Code, 2016",
|
| 112 |
+
"ibc": "Insolvency and Bankruptcy Code, 2016",
|
| 113 |
+
# Both the full name and short name resolve to the same canonical string
|
| 114 |
+
"national company law appellate tribunal rules": "NCLAT Rules, 2016",
|
| 115 |
+
"nclat rules": "NCLAT Rules, 2016",
|
| 116 |
+
"nclat rule": "NCLAT Rules, 2016",
|
| 117 |
+
"constitution of india": "Constitution of India",
|
| 118 |
+
"code of criminal procedure": "Code of Criminal Procedure, 1973",
|
| 119 |
+
"crpc": "Code of Criminal Procedure, 1973",
|
| 120 |
+
"indian penal code": "Indian Penal Code, 1860",
|
| 121 |
+
"ipc": "Indian Penal Code, 1860",
|
| 122 |
+
"civil procedure code": "Code of Civil Procedure, 1908",
|
| 123 |
+
"cpc": "Code of Civil Procedure, 1908",
|
| 124 |
+
"arbitration and conciliation": "Arbitration and Conciliation Act, 1996",
|
| 125 |
+
"right of children": "Right of Children to Free and Compulsory Education Act, 2009",
|
| 126 |
+
"companies act": "Companies Act, 2013",
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
# Acts list normaliser — applied after scraping meta.acts so that
|
| 130 |
+
# "National Company Law Appellate Tribunal Rules, 2016" and
|
| 131 |
+
# "NCLAT Rules, 2016" are unified before section attribution.
|
| 132 |
+
def _normalise_acts(acts: list[str]) -> list[str]:
|
| 133 |
+
"""Deduplicate acts list by resolving all entries through ACT_ALIASES."""
|
| 134 |
+
seen: set[str] = set()
|
| 135 |
+
result: list[str] = []
|
| 136 |
+
for act in acts:
|
| 137 |
+
canonical = act # default: keep as-is
|
| 138 |
+
act_lower = act.lower()
|
| 139 |
+
for key, canon in ACT_ALIASES.items():
|
| 140 |
+
if key in act_lower:
|
| 141 |
+
canonical = canon
|
| 142 |
+
break
|
| 143 |
+
if canonical not in seen:
|
| 144 |
+
seen.add(canonical)
|
| 145 |
+
result.append(canonical)
|
| 146 |
+
return result
|
| 147 |
+
|
| 148 |
+
# Prefix type → which kind of act it belongs to
|
| 149 |
+
# "section" prefixes → statutory acts (IBC, IPC, CPC…)
|
| 150 |
+
# "rule" prefixes → rules/regulations (NCLAT Rules, etc.)
|
| 151 |
+
SECTION_PREFIXES = {"s", "ss", "sec", "section"}
|
| 152 |
+
RULE_PREFIXES = {"r", "rr", "rule"}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ---------------------------------------------------------------------------
|
| 156 |
+
# Helpers
|
| 157 |
+
# ---------------------------------------------------------------------------
|
| 158 |
+
def parse_date(raw: str) -> Optional[str]:
|
| 159 |
+
"""Return ISO-8601 date or None."""
|
| 160 |
+
if not raw:
|
| 161 |
+
return None
|
| 162 |
+
raw = raw.strip()
|
| 163 |
+
for fmt in ("%d %B %Y", "%d %b %Y", "%d-%m-%Y", "%Y-%m-%d"):
|
| 164 |
+
try:
|
| 165 |
+
return datetime.strptime(raw, fmt).strftime("%Y-%m-%d")
|
| 166 |
+
except ValueError:
|
| 167 |
+
pass
|
| 168 |
+
m = re.search(r"(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})", raw)
|
| 169 |
+
if m:
|
| 170 |
+
day, mon, year = m.groups()
|
| 171 |
+
month_num = MONTHS.get(mon.lower())
|
| 172 |
+
if month_num:
|
| 173 |
+
return f"{int(year):04d}-{month_num:02d}-{int(day):02d}"
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def clean_text(text: str) -> str:
|
| 178 |
+
return re.sub(r"\s+", " ", text).strip()
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _resolve_act(prefix: str, sentence_act: Optional[str], known_acts: list[str]) -> str:
|
| 182 |
+
"""
|
| 183 |
+
Determine the most appropriate act for a provision reference.
|
| 184 |
+
Priority: sentence-level act mention > prefix-type hint > first known act.
|
| 185 |
+
"""
|
| 186 |
+
if sentence_act:
|
| 187 |
+
return sentence_act
|
| 188 |
+
|
| 189 |
+
p = prefix.lower().rstrip(".")
|
| 190 |
+
if p in RULE_PREFIXES:
|
| 191 |
+
# Find the first rules/regulations act in known_acts
|
| 192 |
+
for a in known_acts:
|
| 193 |
+
if "rules" in a.lower() or "regulations" in a.lower():
|
| 194 |
+
return a
|
| 195 |
+
return "NCLAT Rules, 2016" # safe default for SCR judgments
|
| 196 |
+
|
| 197 |
+
if p in SECTION_PREFIXES:
|
| 198 |
+
# Find the first non-rules act in known_acts
|
| 199 |
+
for a in known_acts:
|
| 200 |
+
if "rules" not in a.lower() and "regulations" not in a.lower():
|
| 201 |
+
return a
|
| 202 |
+
return known_acts[0] if known_acts else "Unknown Act"
|
| 203 |
+
|
| 204 |
+
return known_acts[0] if known_acts else "Unknown Act"
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# ---------------------------------------------------------------------------
|
| 208 |
+
# Core parser
|
| 209 |
+
# ---------------------------------------------------------------------------
|
| 210 |
+
def parse_judgment_html(html_content: str, source_url: str = "") -> dict:
|
| 211 |
+
"""
|
| 212 |
+
Parse a judgment HTML fragment (from the SCR splitview endpoint)
|
| 213 |
+
and return a fully-populated JudgmentMetadata dict.
|
| 214 |
+
"""
|
| 215 |
+
soup = BeautifulSoup(html_content, "html.parser")
|
| 216 |
+
meta = JudgmentMetadata(source_url=source_url)
|
| 217 |
+
full_text = soup.get_text(separator="\n")
|
| 218 |
+
|
| 219 |
+
# ------------------------------------------------------------------
|
| 220 |
+
# 1. Case name (FIX: was returning file path slug)
|
| 221 |
+
# ------------------------------------------------------------------
|
| 222 |
+
# Try known CSS classes first
|
| 223 |
+
for cls in ["Case-Title", "CaseTitle", "case-title", "Parties", "Party-Name"]:
|
| 224 |
+
el = soup.find(class_=cls)
|
| 225 |
+
if el:
|
| 226 |
+
meta.case_name = clean_text(el.get_text())
|
| 227 |
+
break
|
| 228 |
+
|
| 229 |
+
# Fallback: extract "Appellant v. Respondent" pattern from full text
|
| 230 |
+
if not meta.case_name:
|
| 231 |
+
m = re.search(
|
| 232 |
+
r"([A-Z][A-Za-z\s,\.&]+)\s+[vV][sS]?\.?\s+([A-Z][A-Za-z\s,\.&]+)",
|
| 233 |
+
full_text,
|
| 234 |
+
)
|
| 235 |
+
if m:
|
| 236 |
+
meta.case_name = clean_text(m.group(0))
|
| 237 |
+
|
| 238 |
+
# Fallback: use citation-derived name (strip underscores, page range)
|
| 239 |
+
if not meta.case_name and source_url:
|
| 240 |
+
path_part = source_url.split("path=")[-1]
|
| 241 |
+
# e.g. "2026_5_577_583" → not a useful name, skip
|
| 242 |
+
if not re.match(r"^\d{4}_\d+_\d+", path_part):
|
| 243 |
+
meta.case_name = path_part.replace("_", " ").strip()
|
| 244 |
+
|
| 245 |
+
# ------------------------------------------------------------------
|
| 246 |
+
# 2. Appeal / case number (FIX: broader regex patterns)
|
| 247 |
+
# ------------------------------------------------------------------
|
| 248 |
+
for cls in ["Appeal-No", "AppealNo", "CaseNo", "case-no", "Appeal-Number"]:
|
| 249 |
+
el = soup.find(class_=cls)
|
| 250 |
+
if el:
|
| 251 |
+
meta.appeal_no = clean_text(el.get_text())
|
| 252 |
+
break
|
| 253 |
+
|
| 254 |
+
if not meta.appeal_no:
|
| 255 |
+
# Covers all variants:
|
| 256 |
+
# "Civil Appeal No. 7458 of 2026"
|
| 257 |
+
# "Civil Appeal No(s). 14439-14440 of 2025"
|
| 258 |
+
# "Criminal Appeal Nos. 123-124 of 2025"
|
| 259 |
+
# "Special Leave Petition No. 456 of 2024"
|
| 260 |
+
m = re.search(
|
| 261 |
+
r"((?:Civil|Criminal|Special Leave|Writ)\s+(?:Appeal|Petition)\s+"
|
| 262 |
+
r"No(?:s|\(s\))?\.?\s*[\d\-]+(?:\s*(?:and|&)\s*[\d\-]+)?\s*of\s*\d{4})",
|
| 263 |
+
full_text,
|
| 264 |
+
re.I,
|
| 265 |
+
)
|
| 266 |
+
if m:
|
| 267 |
+
meta.appeal_no = clean_text(m.group(1))
|
| 268 |
+
|
| 269 |
+
# ------------------------------------------------------------------
|
| 270 |
+
# 3. Citation + neutral citation
|
| 271 |
+
# ------------------------------------------------------------------
|
| 272 |
+
cit_el = soup.find(class_="Citation")
|
| 273 |
+
if cit_el:
|
| 274 |
+
raw_cit = clean_text(cit_el.get_text(separator=" "))
|
| 275 |
+
meta.citation = raw_cit
|
| 276 |
+
nc_m = re.search(r"\d{4}\s+INSC\s+\d+", raw_cit)
|
| 277 |
+
if nc_m:
|
| 278 |
+
meta.neutral_citation = nc_m.group(0)
|
| 279 |
+
|
| 280 |
+
# ------------------------------------------------------------------
|
| 281 |
+
# 4. Date
|
| 282 |
+
# ------------------------------------------------------------------
|
| 283 |
+
date_el = soup.find(class_="Date-of-Decision")
|
| 284 |
+
meta.date = parse_date(date_el.get_text(strip=True) if date_el else "")
|
| 285 |
+
|
| 286 |
+
# ------------------------------------------------------------------
|
| 287 |
+
# 5. Bench & author judge (FIX: "JJ." suffix leaking as a judge name)
|
| 288 |
+
# ------------------------------------------------------------------
|
| 289 |
+
coram_el = soup.find(class_="Coram")
|
| 290 |
+
if coram_el:
|
| 291 |
+
raw_bench = clean_text(coram_el.get_text()).strip("[]")
|
| 292 |
+
|
| 293 |
+
# Remove trailing "JJ." / "J." designations before splitting
|
| 294 |
+
# e.g. "Sanjay Kumar* and K. Vinod Chandran, JJ."
|
| 295 |
+
raw_bench = re.sub(r",?\s*JJ?\.$", "", raw_bench, flags=re.I).strip()
|
| 296 |
+
|
| 297 |
+
# Split on " and " or ", "
|
| 298 |
+
judges_raw = re.split(r"\s+and\s+|,\s*", raw_bench, flags=re.I)
|
| 299 |
+
judges_raw = [j.strip() for j in judges_raw if j.strip()]
|
| 300 |
+
|
| 301 |
+
bench_clean = []
|
| 302 |
+
for j in judges_raw:
|
| 303 |
+
is_author = "*" in j
|
| 304 |
+
name = j.replace("*", "").strip()
|
| 305 |
+
if not name:
|
| 306 |
+
continue
|
| 307 |
+
# Append ", J" suffix if not already present
|
| 308 |
+
if not re.search(r",?\s*J\.?$", name, re.I):
|
| 309 |
+
name = name + ", J"
|
| 310 |
+
bench_clean.append(name)
|
| 311 |
+
if is_author and not meta.author_judge:
|
| 312 |
+
meta.author_judge = name
|
| 313 |
+
|
| 314 |
+
meta.bench = bench_clean
|
| 315 |
+
|
| 316 |
+
# If author not marked by asterisk, use first judge as author
|
| 317 |
+
if not meta.author_judge and bench_clean:
|
| 318 |
+
meta.author_judge = bench_clean[0]
|
| 319 |
+
|
| 320 |
+
# ------------------------------------------------------------------
|
| 321 |
+
# 6. Acts — normalised to canonical names to prevent duplicates
|
| 322 |
+
# ------------------------------------------------------------------
|
| 323 |
+
acts_el = soup.find(class_="Acts")
|
| 324 |
+
if acts_el:
|
| 325 |
+
raw_acts = acts_el.get_text(strip=True)
|
| 326 |
+
raw_list = [a.strip().rstrip(".") for a in raw_acts.split(";") if a.strip()]
|
| 327 |
+
meta.acts = _normalise_acts(raw_list)
|
| 328 |
+
|
| 329 |
+
# ------------------------------------------------------------------
|
| 330 |
+
# 7. Sections (structured, act-aware) (FIX: wrong act attribution)
|
| 331 |
+
# ------------------------------------------------------------------
|
| 332 |
+
meta.sections = _extract_sections_structured(soup, meta.acts)
|
| 333 |
+
|
| 334 |
+
# ------------------------------------------------------------------
|
| 335 |
+
# 8. Lower court
|
| 336 |
+
# ------------------------------------------------------------------
|
| 337 |
+
m = re.search(
|
| 338 |
+
r"From the (?:Judgment and )?Order dated[^o]+of the\s+(.+?)\s+in\s+",
|
| 339 |
+
full_text,
|
| 340 |
+
re.I | re.S,
|
| 341 |
+
)
|
| 342 |
+
if m:
|
| 343 |
+
meta.lower_court = clean_text(m.group(1))
|
| 344 |
+
elif "NCLAT" in full_text:
|
| 345 |
+
nm = re.search(r"(National Company Law Appellate Tribunal[^,\n]*)", full_text)
|
| 346 |
+
if nm:
|
| 347 |
+
meta.lower_court = clean_text(nm.group(1))
|
| 348 |
+
elif "High Court" in full_text:
|
| 349 |
+
hm = re.search(r"(High Court of[^,\n]+)", full_text)
|
| 350 |
+
if hm:
|
| 351 |
+
meta.lower_court = clean_text(hm.group(1))
|
| 352 |
+
|
| 353 |
+
# ------------------------------------------------------------------
|
| 354 |
+
# 9. Case type
|
| 355 |
+
# ------------------------------------------------------------------
|
| 356 |
+
acts_lower = " ".join(meta.acts).lower()
|
| 357 |
+
if "insolvency" in acts_lower or "ibc" in acts_lower:
|
| 358 |
+
meta.case_type = "Insolvency / IBC"
|
| 359 |
+
elif "constitution" in acts_lower:
|
| 360 |
+
meta.case_type = "Constitutional"
|
| 361 |
+
elif "criminal procedure" in acts_lower or "ipc" in acts_lower:
|
| 362 |
+
meta.case_type = "Criminal"
|
| 363 |
+
elif "civil procedure" in acts_lower:
|
| 364 |
+
meta.case_type = "Civil"
|
| 365 |
+
elif "arbitration" in acts_lower:
|
| 366 |
+
meta.case_type = "Arbitration"
|
| 367 |
+
elif "tax" in acts_lower or "income" in acts_lower:
|
| 368 |
+
meta.case_type = "Tax"
|
| 369 |
+
elif "labour" in acts_lower or "industrial" in acts_lower:
|
| 370 |
+
meta.case_type = "Labour"
|
| 371 |
+
|
| 372 |
+
# ------------------------------------------------------------------
|
| 373 |
+
# 10. Outcome
|
| 374 |
+
# ------------------------------------------------------------------
|
| 375 |
+
result_el = soup.find(class_="Result")
|
| 376 |
+
if result_el:
|
| 377 |
+
meta.outcome = clean_text(result_el.get_text())
|
| 378 |
+
else:
|
| 379 |
+
tail = full_text[-600:]
|
| 380 |
+
for phrase in [
|
| 381 |
+
"appeals allowed", "appeal allowed",
|
| 382 |
+
"appeals dismissed", "appeal dismissed",
|
| 383 |
+
"petition allowed", "petition dismissed",
|
| 384 |
+
"partly allowed", "disposed of",
|
| 385 |
+
"remanded back", "set aside",
|
| 386 |
+
]:
|
| 387 |
+
if phrase in tail.lower():
|
| 388 |
+
meta.outcome = phrase.title()
|
| 389 |
+
break
|
| 390 |
+
|
| 391 |
+
# ------------------------------------------------------------------
|
| 392 |
+
# 11. Cases cited (FIX: citations were empty)
|
| 393 |
+
# ------------------------------------------------------------------
|
| 394 |
+
meta.cases_cited = _extract_cases_cited(soup)
|
| 395 |
+
|
| 396 |
+
# ------------------------------------------------------------------
|
| 397 |
+
# 12. Keywords
|
| 398 |
+
# ------------------------------------------------------------------
|
| 399 |
+
kw_el = soup.find(class_="Keywords")
|
| 400 |
+
if kw_el:
|
| 401 |
+
raw_kw = kw_el.get_text(strip=True)
|
| 402 |
+
meta.keywords = [k.strip().rstrip(".") for k in raw_kw.split(";") if k.strip()]
|
| 403 |
+
|
| 404 |
+
# ------------------------------------------------------------------
|
| 405 |
+
# 13. Issue + headnote + short summary (FIX: summary was same as issue)
|
| 406 |
+
# ------------------------------------------------------------------
|
| 407 |
+
issue_el = soup.find(class_="Issues-for-Consideration")
|
| 408 |
+
if issue_el:
|
| 409 |
+
meta.issue = clean_text(issue_el.get_text())
|
| 410 |
+
|
| 411 |
+
headnote_els = soup.find_all(class_="Headnote")
|
| 412 |
+
meta.full_headnote = " ".join(
|
| 413 |
+
clean_text(h.get_text(separator=" ")) for h in headnote_els
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
# short_summary = first 2 sentences of headnote "Held:" portion
|
| 417 |
+
meta.short_summary = _make_short_summary(meta.full_headnote, meta.issue)
|
| 418 |
+
|
| 419 |
+
return asdict(meta)
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
# ---------------------------------------------------------------------------
|
| 423 |
+
# Section extraction (FIX: s.61(2) was being attributed to NCLAT Rules)
|
| 424 |
+
# ---------------------------------------------------------------------------
|
| 425 |
+
_PROVISION_RE = re.compile(
|
| 426 |
+
r"\b(section|sec|ss?|rule|rr?)\s*\.?\s*([0-9]+(?:\([0-9A-Za-z]+\))?[A-Za-z]?)",
|
| 427 |
+
re.I,
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
_ACT_MENTION_RE = re.compile(
|
| 431 |
+
r"(insolvency\s+and\s+bankruptcy\s+code"
|
| 432 |
+
r"|nclat\s+rules?"
|
| 433 |
+
r"|national\s+company\s+law\s+appellate\s+tribunal\s+rules?"
|
| 434 |
+
r"|constitution\s+of\s+india"
|
| 435 |
+
r"|code\s+of\s+criminal\s+procedure"
|
| 436 |
+
r"|indian\s+penal\s+code"
|
| 437 |
+
r"|civil\s+procedure\s+code"
|
| 438 |
+
r"|arbitration\s+and\s+conciliation)",
|
| 439 |
+
re.I,
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def _extract_sections_structured(soup: BeautifulSoup, known_acts: list[str]) -> list[dict]:
|
| 444 |
+
"""
|
| 445 |
+
Extract section/rule references with correct act attribution.
|
| 446 |
+
|
| 447 |
+
Strategy:
|
| 448 |
+
- Scan headnote + keywords text sentence by sentence.
|
| 449 |
+
- If a sentence explicitly names an act, attribute all provisions in
|
| 450 |
+
that sentence to that act.
|
| 451 |
+
- Otherwise use prefix type (s/sec → statutory act, r/rule → rules act)
|
| 452 |
+
to pick the right act from known_acts.
|
| 453 |
+
- Deduplicate by (act, number) pair.
|
| 454 |
+
"""
|
| 455 |
+
search_els = (
|
| 456 |
+
soup.find_all(class_="Headnote")
|
| 457 |
+
+ soup.find_all(class_="Keywords")
|
| 458 |
+
+ soup.find_all(class_="Judgment-Body")
|
| 459 |
+
)
|
| 460 |
+
text = " ".join(el.get_text(separator=" ") for el in search_els) if search_els else soup.get_text()
|
| 461 |
+
|
| 462 |
+
sentences = re.split(r"[.;–]\s+", text)
|
| 463 |
+
seen: set[tuple] = set()
|
| 464 |
+
results: list[dict] = []
|
| 465 |
+
|
| 466 |
+
for sentence in sentences:
|
| 467 |
+
# Resolve act context for this sentence
|
| 468 |
+
act_m = _ACT_MENTION_RE.search(sentence)
|
| 469 |
+
sentence_act: Optional[str] = None
|
| 470 |
+
if act_m:
|
| 471 |
+
alias_key = re.sub(r"\s+", " ", act_m.group(0).lower())
|
| 472 |
+
for key, canonical in ACT_ALIASES.items():
|
| 473 |
+
if key in alias_key:
|
| 474 |
+
sentence_act = canonical
|
| 475 |
+
break
|
| 476 |
+
|
| 477 |
+
for m in _PROVISION_RE.finditer(sentence):
|
| 478 |
+
prefix, number = m.group(1), m.group(2)
|
| 479 |
+
act = _resolve_act(prefix, sentence_act, known_acts)
|
| 480 |
+
provision = f"{prefix.lower().rstrip('.')}.{number}"
|
| 481 |
+
key = (act, number)
|
| 482 |
+
if key not in seen:
|
| 483 |
+
seen.add(key)
|
| 484 |
+
results.append(asdict(SectionRef(act=act, provision=provision, number=number)))
|
| 485 |
+
|
| 486 |
+
return results
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
# ---------------------------------------------------------------------------
|
| 490 |
+
# Cases cited (FIX: citation regex wasn't matching inline SCR citations)
|
| 491 |
+
# ---------------------------------------------------------------------------
|
| 492 |
+
_TREATMENT_RE = re.compile(
|
| 493 |
+
r"\b(relied\s+on|referred\s+to|overruled|distinguished|followed|approved|dissented)\b",
|
| 494 |
+
re.I,
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
# Matches: (2022) 2 SCC 244 | [2021] 14 SCR 736 | 2026 INSC 479
|
| 498 |
+
_CITATION_RE = re.compile(
|
| 499 |
+
r"(?:\((\d{4})\)\s*\d+\s+SCC\s+\d+"
|
| 500 |
+
r"|\[(\d{4})\]\s*\d+\s+SCR\s+\d+"
|
| 501 |
+
r"|\d{4}\s+INSC\s+\d+)",
|
| 502 |
+
re.I,
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
# Matches the full citation string for capture
|
| 506 |
+
_CITATION_FULL_RE = re.compile(
|
| 507 |
+
r"(?:\(\d{4}\)\s*\d+\s+SCC\s+\d+"
|
| 508 |
+
r"|\[\d{4}\]\s*\d+\s+SCR\s+\d+"
|
| 509 |
+
r"|\d{4}\s+INSC\s+\d+)",
|
| 510 |
+
re.I,
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def _extract_cases_cited(soup: BeautifulSoup) -> list[dict]:
|
| 515 |
+
"""
|
| 516 |
+
Parse the 'Case Law Cited' section.
|
| 517 |
+
Handles bold/italic case names followed by citations and treatment labels.
|
| 518 |
+
"""
|
| 519 |
+
results: list[dict] = []
|
| 520 |
+
seen: set[str] = set()
|
| 521 |
+
|
| 522 |
+
# Strategy 1: find by CSS class
|
| 523 |
+
case_law_el = soup.find(class_=re.compile(r"Case.?Law|CaseLaw|Cases.?Cited", re.I))
|
| 524 |
+
if case_law_el:
|
| 525 |
+
raw_text = case_law_el.get_text(separator="\n")
|
| 526 |
+
else:
|
| 527 |
+
# Strategy 2: heuristic heading search
|
| 528 |
+
full = soup.get_text(separator="\n")
|
| 529 |
+
m = re.search(
|
| 530 |
+
r"Case Law Cited\s*\n(.*?)(?:\n(?:List of Acts|List of Keywords|Appearances|Judgment)\s*\n|\Z)",
|
| 531 |
+
full,
|
| 532 |
+
re.S | re.I,
|
| 533 |
+
)
|
| 534 |
+
raw_text = m.group(1) if m else ""
|
| 535 |
+
|
| 536 |
+
if not raw_text:
|
| 537 |
+
return results
|
| 538 |
+
|
| 539 |
+
# Each case entry is typically on 1-2 lines; split on blank lines or clear separators
|
| 540 |
+
blocks = re.split(r"\n{2,}|\n(?=[A-Z])", raw_text.strip())
|
| 541 |
+
|
| 542 |
+
for block in blocks:
|
| 543 |
+
block = clean_text(block)
|
| 544 |
+
if not block or len(block) < 10:
|
| 545 |
+
continue
|
| 546 |
+
|
| 547 |
+
# Skip section/list headers
|
| 548 |
+
if re.match(r"^(List of|Case Law|Appearances|Judgment)", block, re.I):
|
| 549 |
+
continue
|
| 550 |
+
|
| 551 |
+
# Find all citations in the block
|
| 552 |
+
all_citations = _CITATION_FULL_RE.findall(block)
|
| 553 |
+
citation_str = " : ".join(all_citations) if all_citations else ""
|
| 554 |
+
|
| 555 |
+
# Treatment
|
| 556 |
+
treatment_m = _TREATMENT_RE.search(block)
|
| 557 |
+
treatment = clean_text(treatment_m.group(0)).lower() if treatment_m else "cited"
|
| 558 |
+
|
| 559 |
+
# Case name: text before the first citation, or before " – relied on" etc.
|
| 560 |
+
name = block
|
| 561 |
+
first_cit_m = _CITATION_FULL_RE.search(block)
|
| 562 |
+
if first_cit_m:
|
| 563 |
+
name = block[: first_cit_m.start()]
|
| 564 |
+
# Also cut at treatment label
|
| 565 |
+
treatment_pos = _TREATMENT_RE.search(name)
|
| 566 |
+
if treatment_pos:
|
| 567 |
+
name = name[: treatment_pos.start()]
|
| 568 |
+
|
| 569 |
+
# Clean up trailing punctuation / dashes / SCR refs
|
| 570 |
+
name = re.sub(r"\s*[–\-:]+\s*$", "", name)
|
| 571 |
+
name = re.sub(r"\s*\[?\d{4}\]?\s*\d*\s*S[CR]{2}.*", "", name)
|
| 572 |
+
name = clean_text(name)
|
| 573 |
+
|
| 574 |
+
if not name or name in seen or len(name) < 5:
|
| 575 |
+
continue
|
| 576 |
+
seen.add(name)
|
| 577 |
+
|
| 578 |
+
results.append(asdict(CaseCited(name=name, citation=citation_str, treatment=treatment)))
|
| 579 |
+
|
| 580 |
+
return results
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
# ---------------------------------------------------------------------------
|
| 584 |
+
# Short summary (FIX: was identical to issue; now derived from headnote)
|
| 585 |
+
# ---------------------------------------------------------------------------
|
| 586 |
+
def _make_short_summary(headnote: str, issue: str) -> str:
|
| 587 |
+
"""
|
| 588 |
+
Extract a 2-3 sentence summary from the 'Held:' portion of the headnote.
|
| 589 |
+
Falls back to the first 2 sentences of the headnote, then to the issue.
|
| 590 |
+
"""
|
| 591 |
+
if headnote:
|
| 592 |
+
# Prefer the "Held:" conclusion
|
| 593 |
+
held_m = re.search(r"\bHeld\s*:\s*(.+?)(?:\[Para|\Z)", headnote, re.S | re.I)
|
| 594 |
+
base = held_m.group(1) if held_m else headnote
|
| 595 |
+
|
| 596 |
+
sentences = re.split(r"(?<=[.!?])\s+–?\s*", base.strip())
|
| 597 |
+
summary = " ".join(sentences[:3]).strip()
|
| 598 |
+
|
| 599 |
+
if len(summary) > 500:
|
| 600 |
+
summary = summary[:500].rsplit(" ", 1)[0] + "…"
|
| 601 |
+
if summary:
|
| 602 |
+
return summary
|
| 603 |
+
|
| 604 |
+
# Final fallback: issue
|
| 605 |
+
return issue[:400] + "…" if len(issue) > 400 else issue
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
# ---------------------------------------------------------------------------
|
| 609 |
+
# HTTP layer
|
| 610 |
+
# ---------------------------------------------------------------------------
|
| 611 |
+
BASE_URL = "https://scr.sci.gov.in/scrsearch/"
|
| 612 |
+
HEADERS = {
|
| 613 |
+
"User-Agent": (
|
| 614 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 615 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 616 |
+
"Chrome/124.0.0.0 Safari/537.36"
|
| 617 |
+
),
|
| 618 |
+
"Accept-Language": "en-US,en;q=0.9",
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
def build_session() -> requests.Session:
|
| 623 |
+
s = requests.Session()
|
| 624 |
+
s.headers.update(HEADERS)
|
| 625 |
+
return s
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
def fetch_homepage(session: requests.Session) -> BeautifulSoup:
|
| 629 |
+
log.info("Fetching SCR homepage...")
|
| 630 |
+
r = session.get(BASE_URL, timeout=30)
|
| 631 |
+
r.raise_for_status()
|
| 632 |
+
return BeautifulSoup(r.text, "html.parser")
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
def download_captcha(session: requests.Session, soup: BeautifulSoup, out_path: Path) -> None:
|
| 636 |
+
captcha_img = soup.find(id="captcha_image")
|
| 637 |
+
if not captcha_img:
|
| 638 |
+
raise RuntimeError("CAPTCHA image element not found on homepage.")
|
| 639 |
+
url = f"https://scr.sci.gov.in{captcha_img.get('src', '')}"
|
| 640 |
+
r = session.get(url, timeout=15)
|
| 641 |
+
r.raise_for_status()
|
| 642 |
+
out_path.write_bytes(r.content)
|
| 643 |
+
log.info(f"CAPTCHA saved → {out_path.resolve()}")
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
def verify_captcha(session: requests.Session, captcha_code: str, search_text: str) -> str:
|
| 647 |
+
payload = {
|
| 648 |
+
"captcha": captcha_code, "search_text": search_text,
|
| 649 |
+
"search_opt": "PHRASE", "escr_flag": "", "proximity": "",
|
| 650 |
+
"sel_lang": "", "neu_cit_year": "", "neu_no": "", "ncn": "",
|
| 651 |
+
"citation_vol": "", "citation_year": "", "citation_supl": "",
|
| 652 |
+
"citation_page": "", "ajax_req": "true", "app_token": "",
|
| 653 |
+
}
|
| 654 |
+
r = session.post(
|
| 655 |
+
f"{BASE_URL}?p=pdf_search/checkCaptcha", data=payload,
|
| 656 |
+
headers={"X-Requested-With": "XMLHttpRequest"}, timeout=30,
|
| 657 |
+
)
|
| 658 |
+
r.raise_for_status()
|
| 659 |
+
data = r.json()
|
| 660 |
+
if data.get("captcha_status") != "Y":
|
| 661 |
+
raise ValueError("CAPTCHA verification failed.")
|
| 662 |
+
return data.get("app_token", "")
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
def init_search_session(session, search_text, captcha_code, app_token):
|
| 666 |
+
params = {
|
| 667 |
+
"p": "pdf_search/home", "text": search_text, "captcha": captcha_code,
|
| 668 |
+
"search_opt": "PHRASE", "fcourt_type": "3", "escr_flag": "", "app_token": app_token,
|
| 669 |
+
}
|
| 670 |
+
session.get(BASE_URL, params=params, timeout=30).raise_for_status()
|
| 671 |
+
log.info("Search session initialized.")
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def fetch_results_list(session, app_token, start=0, length=50):
|
| 675 |
+
"""Fetch a specific paginated batch of results."""
|
| 676 |
+
payload = {
|
| 677 |
+
"p": "pdf_search/home/", "sEcho": "1", "iColumns": "2", "sColumns": ",",
|
| 678 |
+
"iDisplayStart": str(start), "iDisplayLength": str(length),
|
| 679 |
+
"fcourt_type": "3", "search_opt": "PHRASE", "ajax_req": "true", "app_token": app_token,
|
| 680 |
+
}
|
| 681 |
+
r = session.post(
|
| 682 |
+
f"{BASE_URL}?p=pdf_search/home/", data=payload,
|
| 683 |
+
headers={"X-Requested-With": "XMLHttpRequest"}, timeout=30,
|
| 684 |
+
)
|
| 685 |
+
r.raise_for_status()
|
| 686 |
+
return r.json().get("reportrow", {}).get("aaData", [])
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
def fetch_splitview(session, args, app_token):
|
| 690 |
+
payload = {
|
| 691 |
+
"val": args[0], "citation_year": args[1], "path": args[2],
|
| 692 |
+
"fcourt_type": "3", "nc_display": args[3], "flag": args[4],
|
| 693 |
+
"ajax_req": "true", "app_token": app_token,
|
| 694 |
+
}
|
| 695 |
+
r = session.post(
|
| 696 |
+
f"{BASE_URL}?p=pdf_search/splitview", data=payload,
|
| 697 |
+
headers={"X-Requested-With": "XMLHttpRequest"}, timeout=30,
|
| 698 |
+
)
|
| 699 |
+
r.raise_for_status()
|
| 700 |
+
return r.json().get("outputfile", "")
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
def parse_splitview_args(row_html: str) -> Optional[list]:
|
| 704 |
+
soup = BeautifulSoup(row_html, "html.parser")
|
| 705 |
+
btn = soup.find("a", onclick=lambda x: x and "open_splitview" in x and "'H'" in x)
|
| 706 |
+
if not btn:
|
| 707 |
+
return None
|
| 708 |
+
m = re.search(r"open_splitview\(([^)]+)\)", btn.get("onclick", ""))
|
| 709 |
+
if not m:
|
| 710 |
+
return None
|
| 711 |
+
args = [a.strip().strip("'\"") for a in m.group(1).split(",")]
|
| 712 |
+
return args if len(args) >= 5 else None
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def fetch_pdf_from_splitview(session, html_content, path, app_token, pdf_dir):
|
| 716 |
+
from pathlib import Path
|
| 717 |
+
import re
|
| 718 |
+
|
| 719 |
+
pdf_dir = Path(pdf_dir)
|
| 720 |
+
pdf_dir.mkdir(parents=True, exist_ok=True)
|
| 721 |
+
|
| 722 |
+
# Extract hidden field values from splitview HTML
|
| 723 |
+
year_m = re.search(r"name='year'[^>]*value='(\d+)'", html_content)
|
| 724 |
+
vol_m = re.search(r"name='volume'[^>]*value='(\d+)'", html_content)
|
| 725 |
+
part_m = re.search(r"name='partno'[^>]*value='(\d+)'", html_content)
|
| 726 |
+
|
| 727 |
+
year = year_m.group(1) if year_m else path.split("_")[0]
|
| 728 |
+
volume = vol_m.group(1) if vol_m else path.split("_")[1]
|
| 729 |
+
part = part_m.group(1) if part_m else path.split("_")[2]
|
| 730 |
+
|
| 731 |
+
# path = "2026_5_577_583" → pages = "577_583"
|
| 732 |
+
parts = path.split("_")
|
| 733 |
+
pages = f"{parts[2]}_{parts[3]}" if len(parts) >= 4 else path
|
| 734 |
+
|
| 735 |
+
candidate_urls = [
|
| 736 |
+
f"https://scr.sci.gov.in/scrsearch/pdfs/{year}/{volume}/{pages}.pdf",
|
| 737 |
+
f"https://scr.sci.gov.in/scrsearch/pdfs/{path}.pdf",
|
| 738 |
+
f"https://scr.sci.gov.in/scrsearch/pdfs/{year}_{volume}_{pages}.pdf",
|
| 739 |
+
f"https://scr.sci.gov.in/scrsearch/?p=pdf_search/viewpdf&year={year}&volume={volume}&partno={part}&app_token={app_token}",
|
| 740 |
+
]
|
| 741 |
+
|
| 742 |
+
for url in candidate_urls:
|
| 743 |
+
try:
|
| 744 |
+
r = session.get(url, timeout=30)
|
| 745 |
+
if r.status_code == 200 and r.content[:4] == b"%PDF":
|
| 746 |
+
pdf_path = pdf_dir / f"{path}.pdf"
|
| 747 |
+
pdf_path.write_bytes(r.content)
|
| 748 |
+
log.info(f" ✓ PDF saved ({len(r.content) // 1024} KB) from: {url}")
|
| 749 |
+
return str(pdf_path)
|
| 750 |
+
else:
|
| 751 |
+
log.info(f" Not a PDF at: {url} (status={r.status_code})")
|
| 752 |
+
except Exception as e:
|
| 753 |
+
log.info(f" Failed: {url} → {e}")
|
| 754 |
+
|
| 755 |
+
log.warning(f" No PDF found for: {path}")
|
| 756 |
+
return ""
|
| 757 |
+
# ---------------------------------------------------------------------------
|
| 758 |
+
# Rewritten Main Function
|
| 759 |
+
# ---------------------------------------------------------------------------
|
| 760 |
+
def main():
|
| 761 |
+
Path("data/html").mkdir(parents=True, exist_ok=True)
|
| 762 |
+
session = build_session()
|
| 763 |
+
|
| 764 |
+
# Switch to JSON Lines (.jsonl) for incremental, crash-proof saving
|
| 765 |
+
out_path = Path("data/html/extracted_judgments.jsonl")
|
| 766 |
+
err_path = Path("data/html/errors.jsonl")
|
| 767 |
+
|
| 768 |
+
# 1. Auto-Resume Check
|
| 769 |
+
start_offset = 0
|
| 770 |
+
if out_path.exists():
|
| 771 |
+
with open(out_path, "r", encoding="utf-8") as f:
|
| 772 |
+
start_offset = sum(1 for _ in f)
|
| 773 |
+
if start_offset > 0:
|
| 774 |
+
log.info(f"Found {start_offset} existing records. Resuming from there.")
|
| 775 |
+
|
| 776 |
+
# 2. Homepage + CAPTCHA
|
| 777 |
+
try:
|
| 778 |
+
homepage_soup = fetch_homepage(session)
|
| 779 |
+
except Exception as e:
|
| 780 |
+
log.error(f"Could not reach SCR homepage: {e}")
|
| 781 |
+
return
|
| 782 |
+
|
| 783 |
+
captcha_path = Path("data/html/captcha.png")
|
| 784 |
+
try:
|
| 785 |
+
download_captcha(session, homepage_soup, captcha_path)
|
| 786 |
+
if sys.platform == "win32":
|
| 787 |
+
os.startfile(str(captcha_path.resolve()))
|
| 788 |
+
except Exception as e:
|
| 789 |
+
log.error(f"CAPTCHA download failed: {e}")
|
| 790 |
+
return
|
| 791 |
+
|
| 792 |
+
# 3. Collect inputs
|
| 793 |
+
search_text = input("Enter search keyword [insolvency]: ").strip() or "insolvency"
|
| 794 |
+
max_results_raw = input("How many total judgments to scrape? [1000]: ").strip()
|
| 795 |
+
max_results = int(max_results_raw) if max_results_raw.isdigit() else 1000
|
| 796 |
+
|
| 797 |
+
if start_offset >= max_results:
|
| 798 |
+
log.info("Target number of judgments already reached in previous runs. Exiting.")
|
| 799 |
+
return
|
| 800 |
+
|
| 801 |
+
# 4. CAPTCHA verification loop
|
| 802 |
+
app_token = None
|
| 803 |
+
for attempt in range(1, 4):
|
| 804 |
+
captcha_code = input(f"Enter CAPTCHA code (attempt {attempt}/3): ").strip()
|
| 805 |
+
if not captcha_code:
|
| 806 |
+
continue
|
| 807 |
+
try:
|
| 808 |
+
app_token = verify_captcha(session, captcha_code, search_text)
|
| 809 |
+
log.info("CAPTCHA verified successfully.")
|
| 810 |
+
break
|
| 811 |
+
except ValueError as e:
|
| 812 |
+
log.warning(f"Attempt {attempt} failed: {e}")
|
| 813 |
+
if attempt < 3:
|
| 814 |
+
try:
|
| 815 |
+
homepage_soup = fetch_homepage(session)
|
| 816 |
+
download_captcha(session, homepage_soup, captcha_path)
|
| 817 |
+
if sys.platform == "win32":
|
| 818 |
+
os.startfile(str(captcha_path.resolve()))
|
| 819 |
+
except Exception as ref_e:
|
| 820 |
+
log.error(f"CAPTCHA refresh failed: {ref_e}")
|
| 821 |
+
|
| 822 |
+
if app_token is None:
|
| 823 |
+
log.error("All CAPTCHA attempts failed. Exiting.")
|
| 824 |
+
return
|
| 825 |
+
|
| 826 |
+
# 5. Init search session
|
| 827 |
+
try:
|
| 828 |
+
init_search_session(session, search_text, captcha_code, app_token)
|
| 829 |
+
except Exception as e:
|
| 830 |
+
log.error(f"Search session init failed: {e}")
|
| 831 |
+
return
|
| 832 |
+
|
| 833 |
+
# 6. Paginated Fetch & Incremental Save
|
| 834 |
+
batch_size = 50
|
| 835 |
+
records_fetched = start_offset
|
| 836 |
+
|
| 837 |
+
log.info(f"Targeting {max_results} judgments. Starting from index {records_fetched}...")
|
| 838 |
+
|
| 839 |
+
while records_fetched < max_results:
|
| 840 |
+
fetch_count = min(batch_size, max_results - records_fetched)
|
| 841 |
+
log.info(f"\n--- Fetching batch: {records_fetched} to {records_fetched + fetch_count - 1} ---")
|
| 842 |
+
|
| 843 |
+
# Fetch the page chunk
|
| 844 |
+
try:
|
| 845 |
+
rows = fetch_results_list(session, app_token, start=records_fetched, length=fetch_count)
|
| 846 |
+
except Exception as e:
|
| 847 |
+
log.error(f"Failed to fetch results batch at offset {records_fetched}: {e}")
|
| 848 |
+
log.info("Session may have timed out. Restart the script; it will auto-resume where it left off.")
|
| 849 |
+
break
|
| 850 |
+
|
| 851 |
+
if not rows:
|
| 852 |
+
log.info("No more results returned by the server. Search exhausted.")
|
| 853 |
+
break
|
| 854 |
+
|
| 855 |
+
# Process the chunk
|
| 856 |
+
for idx, row in enumerate(rows, 1):
|
| 857 |
+
row_html = row[1] if len(row) > 1 else ""
|
| 858 |
+
args = parse_splitview_args(row_html)
|
| 859 |
+
current_global_idx = records_fetched + idx
|
| 860 |
+
|
| 861 |
+
if not args:
|
| 862 |
+
log.warning(f"[{current_global_idx}] Could not parse splitview args. Skipping.")
|
| 863 |
+
with open(err_path, "a", encoding="utf-8") as ef:
|
| 864 |
+
ef.write(json.dumps({"index": current_global_idx, "reason": "no splitview args"}) + "\n")
|
| 865 |
+
continue
|
| 866 |
+
|
| 867 |
+
citation_path = args[2]
|
| 868 |
+
log.info(f"[{current_global_idx}/{max_results}] Parsing: {citation_path}")
|
| 869 |
+
|
| 870 |
+
try:
|
| 871 |
+
html_content = fetch_splitview(session, args, app_token)
|
| 872 |
+
source_url = f"{BASE_URL}?p=pdf_search/splitview&path={citation_path}"
|
| 873 |
+
metadata = parse_judgment_html(html_content, source_url=source_url)
|
| 874 |
+
|
| 875 |
+
metadata["pdf_path"] = fetch_pdf_from_splitview(
|
| 876 |
+
session, html_content, citation_path, app_token, pdf_dir="data/pdfs"
|
| 877 |
+
)
|
| 878 |
+
# Incremental Save: Append to JSONL immediately
|
| 879 |
+
with open(out_path, "a", encoding="utf-8") as f:
|
| 880 |
+
f.write(json.dumps(metadata, ensure_ascii=False) + "\n")
|
| 881 |
+
|
| 882 |
+
except Exception as e:
|
| 883 |
+
log.error(f"[{current_global_idx}] Failed: {e}")
|
| 884 |
+
with open(err_path, "a", encoding="utf-8") as ef:
|
| 885 |
+
ef.write(json.dumps({"index": current_global_idx, "path": citation_path, "reason": str(e)}) + "\n")
|
| 886 |
+
|
| 887 |
+
# Dynamic human-like delay to prevent IP blocking
|
| 888 |
+
time.sleep(random.uniform(1.0, 2.5))
|
| 889 |
+
|
| 890 |
+
records_fetched += len(rows)
|
| 891 |
+
|
| 892 |
+
log.info(f"\n✓ Process stopped. Data securely saved to {out_path.resolve()}")
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
if __name__ == "__main__":
|
| 896 |
+
main()
|
phase1/.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# derived eval run artifacts (regenerable; not source)
|
| 2 |
+
eval/auth_*.tsv
|
| 3 |
+
eval/agent_*.tsv
|
| 4 |
+
eval/authority_baseline.tsv
|
| 5 |
+
eval/authority_spine.tsv
|
| 6 |
+
eval/silver_baseline.tsv
|
| 7 |
+
eval/silver_spine.tsv
|
| 8 |
+
eval/run.tsv
|
| 9 |
+
eval/run_baseline.tsv
|
| 10 |
+
eval/plan_*.json
|
| 11 |
+
eval/last_score.json
|
| 12 |
+
__pycache__/
|
| 13 |
+
scripts/__pycache__/
|
| 14 |
+
eval/pagerank.json
|
| 15 |
+
phase1/COSTING.md
|
| 16 |
+
phase1/Themis_Costing.pdf
|
phase1/AGENTIC_SEARCH_SPEC.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Themis — Agentic Search Spec (the "controller" front door)
|
| 2 |
+
|
| 3 |
+
Status: **DRAFT for founder verification** (2026-06-25). Once George signs off, build proceeds
|
| 4 |
+
stage-by-stage, each behind an eval gate, then to Hitin's pilot.
|
| 5 |
+
|
| 6 |
+
Companion docs: `HANDOFF.md` (current system + autoresearch method), `eval/` (the measurement rig).
|
| 7 |
+
This spec describes what *replaces* the split fast/deep endpoints with one adaptive, self-correcting loop.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 0. The idea in one paragraph
|
| 12 |
+
|
| 13 |
+
Search today is a **fixed pipeline**: retrieve → rerank → verify → (maybe one requery) → answer. It commits
|
| 14 |
+
to its first retrieval and can't notice when it missed the controlling authority. We replace the front door
|
| 15 |
+
with a **bounded control loop** that keeps a working "brief," *inspects its own results after each round
|
| 16 |
+
against a legal-quality signal*, and takes a corrective action when the brief is weak — until it has a
|
| 17 |
+
high-authority, good-law, grounded answer or hits its round/time budget. Easy queries stop in ~2s; hard ones
|
| 18 |
+
spend up to ~12s with **visible progress**. The proven authority prior (+0.072 nDCG@10 on doctrinal queries,
|
| 19 |
+
§7b of HANDOFF) and the good-law filter are *tools the loop wields*, query-routed, not a global setting.
|
| 20 |
+
|
| 21 |
+
**Latency budget:** soft 12s, hard 15s wall-clock, with a progress event per action. (Founder call, 2026-06-25.)
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## 1. What we KEEP (proven primitives — do not rebuild)
|
| 26 |
+
|
| 27 |
+
All of these exist in `scripts/serve.py` and are reused verbatim as the loop's tools:
|
| 28 |
+
|
| 29 |
+
| Primitive | Function | Role in the loop |
|
| 30 |
+
|---|---|---|
|
| 31 |
+
| Hybrid retrieval | `candidates()` (dense BGE + BM25, RRF) | base `search` action |
|
| 32 |
+
| Cross-encoder rerank | `rerank()` | scores any candidate set |
|
| 33 |
+
| Relevance screen | `verify()` (DeepSeek paralegal) | gates what enters the brief |
|
| 34 |
+
| Identity lookup | `identity_hits()` / `name_search()` / `id_card()` | the known-item fast exit |
|
| 35 |
+
| Plan | `_plan()` → sub-issues + expected authority names | seeds the brief's `doctrine` + `expected_authorities` |
|
| 36 |
+
| Citation graph | `cites_docs()` / `cited_by_docs()` + `edge_meta` (treatment) | the `walk_citations` action |
|
| 37 |
+
| Authority signal | `cite_indeg` (CITE in-degree) | the validated ranking prior |
|
| 38 |
+
| Good-law | `goodlaw[doc]` (status + provenance + treatment) | filter / flag / pivot |
|
| 39 |
+
| Card for a specific doc | `card_for_doc()` | scores graph/authority additions |
|
| 40 |
+
| Grounding gate | `grounded_answer()` / `verify_claims()` (verbatim substring) | the answer invariant — unchanged |
|
| 41 |
+
| Progress protocol | SSE `{t:"step", k, s, label}` + `answer_delta`/`claims`/`dropped_claims` | the live UI |
|
| 42 |
+
|
| 43 |
+
**The grounding invariant is non-negotiable and already correct:** the loop may *add* candidates freely, but the
|
| 44 |
+
user-facing answer is rendered only from claims whose ≥4-word quote is a verbatim substring of a loaded case.
|
| 45 |
+
Self-correction therefore cannot amplify a hallucination — the worst a bad round does is waste budget.
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## 2. The working brief (loop state)
|
| 50 |
+
|
| 51 |
+
A single dict accumulated across rounds — the "layered understanding":
|
| 52 |
+
|
| 53 |
+
```
|
| 54 |
+
brief = {
|
| 55 |
+
"q": original query,
|
| 56 |
+
"intent": one of {known_item, doctrinal, factual, mixed}, # set once, round 0 (see §3)
|
| 57 |
+
"doctrine": [issue phrases], # from _plan
|
| 58 |
+
"statute_refs": [section ids], # detected + concordance-resolved (Stage 2)
|
| 59 |
+
"expected_authorities": [case names], # from _plan; each grounded via name_search
|
| 60 |
+
"pool": {doc_id: card}, # everything retrieved so far, de-duped
|
| 61 |
+
"confirmed": [doc_id, ...], # relevant ∧ good-law ∧ on-point → the answer set
|
| 62 |
+
"gaps": [gap-tokens], # what's missing; drives the next action
|
| 63 |
+
"rounds": int,
|
| 64 |
+
"spent_ms": int,
|
| 65 |
+
}
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
`gaps` is the engine of self-correction — each round recomputes it, and the controller picks the action that
|
| 69 |
+
closes the biggest gap.
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## 3. Round 0 — intent (the "router," reframed)
|
| 74 |
+
|
| 75 |
+
The router you flagged as too dumb becomes a **state-setter, not a gate**. One cheap DeepSeek call (or a
|
| 76 |
+
rule+embedding shortcut) labels `intent` and extracts `statute_refs`. It does **not** decide the whole strategy
|
| 77 |
+
— it just initializes the brief and selects defaults:
|
| 78 |
+
|
| 79 |
+
- `known_item` (a name/citation) → exact-lookup exit (`identity_hits`), no loop. Already works; success@1 guardrail.
|
| 80 |
+
- `doctrinal` / `mixed` → authority prior **ON** (α=0.3), full loop eligible.
|
| 81 |
+
- `factual` (narrow fact pattern, no landmark expected) → authority prior **OFF** (it's −0.29 on general silver),
|
| 82 |
+
loop still eligible for coverage but not authority-boosted.
|
| 83 |
+
|
| 84 |
+
Why route, not globally apply: the prior is **+0.072 on doctrinal, −0.29 on general** — it must be conditional.
|
| 85 |
+
The intent label is the cheapest correct place to make that switch.
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## 4. The control loop
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
brief = init(q) # round 0: intent, _plan → doctrine + expected_authorities, statute_refs
|
| 93 |
+
if brief.intent == known_item: return identity_exit(brief)
|
| 94 |
+
|
| 95 |
+
round 1: brief.pool += search(q); verify(); brief.confirmed = keep(); rank(brief)
|
| 96 |
+
while not STOP(brief): # rounds 2..MAX
|
| 97 |
+
action = choose(brief) # the self-correction policy (§5)
|
| 98 |
+
if action is None: break # nothing would close the gap → stop (don't spin)
|
| 99 |
+
emit_step(action.label) # visible progress
|
| 100 |
+
brief.pool += action.run(brief); verify(new); brief.confirmed = keep(); rank(brief)
|
| 101 |
+
answer = ground(brief.confirmed) # the verbatim gate, unchanged
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
**STOP** (any one fires):
|
| 105 |
+
- coverage ok: ≥3 relevant good-law cases **and** an authority is present (top-of-list cite_indeg ≥ doctrine
|
| 106 |
+
threshold, or an `expected_authority` resolved into `confirmed`) **and** the grounding gate verified ≥1 holding;
|
| 107 |
+
- `rounds ≥ MAX` (default 3);
|
| 108 |
+
- `spent_ms ≥ 12000` (soft) / hard cut at 15000;
|
| 109 |
+
- `choose()` returns None (no remaining gap is addressable).
|
| 110 |
+
|
| 111 |
+
**rank(brief):** `score = sigmoid(rr) + (α·log1p(cite_indeg[d]) if intent∈{doctrinal,mixed} else 0)`, then drop
|
| 112 |
+
or sink any `good_law_status ∈ {overruled, partly_overruled, per_incuriam}` unless the query is *about* that case.
|
| 113 |
+
This is exactly the §7b commit config (authority prior + good-law filter), now applied every round.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## 5. The self-correction policy — `choose(brief)`
|
| 118 |
+
|
| 119 |
+
After each round, compute the gap signal (all cheap / mechanical) and pick the action addressing the biggest gap.
|
| 120 |
+
Each maps a *detected deficiency in the previous output* to a corrective tool — this is the "self-corrects based
|
| 121 |
+
on its previous outputs" behaviour, made concrete and legally meaningful:
|
| 122 |
+
|
| 123 |
+
| Detected gap (signal) | Action | Tool(s) |
|
| 124 |
+
|---|---|---|
|
| 125 |
+
| No high-authority case in `confirmed` (top cite_indeg below doctrine threshold) → likely missed the landmark | pull expected authorities + draft-the-holding requery | `name_search(expected_authorities)`, **`hyde(q)`** → `search` |
|
| 126 |
+
| An `expected_authority` was named but never retrieved | targeted name lookup + walk in from its nearest neighbour | `name_search(name)`, `walk_citations(near, in)` |
|
| 127 |
+
| Top result flagged overruled, no live replacement | pivot to current law | `walk_citations(overruling_edge)`, `good_law` |
|
| 128 |
+
| `statute_refs` present but no statute/section pulled | statute pivot | **`statute_lookup(section)`** (Stage 2) |
|
| 129 |
+
| `confirmed` splits into ≥2 disjoint citation clusters (conflicting lines) | fetch the resolver | larger-bench / later case via `walk_citations` |
|
| 130 |
+
| Thin coverage (<3 kept) but authority present | one rephrase (the existing move) | `requery` (LLM rewrite) → `search` |
|
| 131 |
+
| None of the above, gap remains | **stop** (return None) | — |
|
| 132 |
+
|
| 133 |
+
`hyde(q)` and `statute_lookup()` are the only genuinely new retrieval tools; everything else is an existing
|
| 134 |
+
function called from a new policy. Each action is tried **at most once** per request (no loops on the same move).
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## 6. The eval harness (every stage clears a gate — autoresearch discipline)
|
| 139 |
+
|
| 140 |
+
The current rig scores a *static* run.tsv. To measure a *loop*, we add **`eval/agentic_run.py`**: imports the
|
| 141 |
+
controller, runs the full loop per query (DeepSeek + tools live), emits the final ranked `confirmed` as run.tsv →
|
| 142 |
+
scored by the existing `score_qrels.py`. Same frozen qrels, no LLM at score time, judge family ≠ serving family.
|
| 143 |
+
|
| 144 |
+
**Slices:**
|
| 145 |
+
- `authority_*` (150 doctrinal) — where the loop should shine; primary nDCG@10.
|
| 146 |
+
- `queries.tsv` (800 silver) — general; must **not** regress.
|
| 147 |
+
- **NEW `recall_recovery_*`** (~40 queries whose gold landmark is *outside* dense top-100) — the loop's whole
|
| 148 |
+
reason to exist; measures recall the one-shot spine *cannot* achieve. Built by scanning doctrinal queries for
|
| 149 |
+
gold docs absent from the dense pool (e.g. the Maneka Gandhi / Royappa / Indra Sawhney pool-misses).
|
| 150 |
+
|
| 151 |
+
**Metrics & guardrails:**
|
| 152 |
+
- primary: nDCG@10 (authority slice ↑, silver flat), recall@10 on recall-recovery (↑).
|
| 153 |
+
- guardrails: bad-law@10 (good-law precision, must stay ≤ baseline), success@1 on known-item (identity route),
|
| 154 |
+
grounding rate (% queries with ≥1 verified holding), latency p50/p95.
|
| 155 |
+
- **agentic-specific: marginal lift per round** — nDCG of `confirmed` after round 1 vs round 2 vs final, computed
|
| 156 |
+
only over queries where each round actually fired. If round N doesn't beat round N−1 on its own fired set, that
|
| 157 |
+
self-correction move is **cut**. This is how we prove the loop earns its latency rather than assuming it.
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## 7. Stagewise build plan
|
| 162 |
+
|
| 163 |
+
Each stage is independently shippable and measured. Stages 0–1 are the **thin pilot cut for Hitin**.
|
| 164 |
+
|
| 165 |
+
### Stage 0 — fold the proven levers into one measured spine *(foundation; ~2 days)*
|
| 166 |
+
- Bake the authority prior into `rank` (α=0.3) **query-routed** by a round-0 intent label.
|
| 167 |
+
- Make the good-law filter **real in the fast path** (drop/flag overruled — closes the F6 hole; deep mode already does it).
|
| 168 |
+
- Ship `eval/agentic_run.py` so everything downstream is measured end-to-end.
|
| 169 |
+
- **Gate:** authority nDCG@10 ≥ 0.35 (proved 0.351), silver no regression, bad-law@10 ≤ baseline, success@1 held.
|
| 170 |
+
- *Already a shippable improvement; no loop yet.*
|
| 171 |
+
|
| 172 |
+
### Stage 1 — the controller + ONE self-correction round + progress *(the pilot cut; ~3–4 days)*
|
| 173 |
+
- Introduce `scripts/agent.py`: the brief, the bounded loop, STOP rules, the intent state-setter.
|
| 174 |
+
- Implement one corrective move first: **missed-landmark / thin-coverage → `name_search(expected_authorities)` +
|
| 175 |
+
`hyde(q)` requery**. (Reuses `_plan`; `hyde` is new and small.)
|
| 176 |
+
- Collapse fast/deep into one adaptive endpoint: round-1-only for easy queries (~2s), escalate for hard (~12s).
|
| 177 |
+
- Emit a progress step per action — the self-correction is *visible* (the trust-builder and the demo magic).
|
| 178 |
+
- **Gate:** on authority + recall-recovery slices, loop final nDCG ≥ Stage-0 spine **and** recall-recovery ↑;
|
| 179 |
+
latency p95 ≤ 15s; marginal lift of round-2 positive on its fired queries. → **hand to Hitin.**
|
| 180 |
+
|
| 181 |
+
### Stage 2 — the rest of the self-correction moves (layered understanding) *(~1 week)*
|
| 182 |
+
- **Statute layer:** embed `statute corpus/all_statutes.json` (2,353 sections — cheap) + a BNS↔IPC/BNSS↔CrPC/
|
| 183 |
+
BSA↔Evidence concordance table; wire `statute_lookup()`. Add the statute-pivot move.
|
| 184 |
+
- Add overruled-pivot and conflict-resolution moves. Each added **behind its own marginal-lift gate** — a move
|
| 185 |
+
that doesn't move its target subset is cut, not kept "because it's principled."
|
| 186 |
+
|
| 187 |
+
### Stage 3 — ranking & answer polish *(measured, post-pilot)*
|
| 188 |
+
- PageRank / Personalized-PageRank authority (stronger than raw cite_indeg; gated on clean Tier-1/2 edges).
|
| 189 |
+
- Headnote-fed `verify` (down-weight, don't drop, high-authority); cross-family entailment gate after the
|
| 190 |
+
verbatim gate.
|
| 191 |
+
|
| 192 |
+
### Stage 4 — promote eval to gold + retune
|
| 193 |
+
- Hitin audits a stratified sample of authority+silver+recall-recovery → kappa → gold. Retune α, STOP thresholds,
|
| 194 |
+
round budget, doctrine cite_indeg threshold against gold rather than silver.
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 8. Where a self-correcting loop could make legal answers WORSE (panel concerns + mitigations)
|
| 199 |
+
|
| 200 |
+
- **Hypothesis lock-in** — a wrong round-0 `_plan` (hallucinated authority names) steers every later round.
|
| 201 |
+
*Mitigation:* names are candidates only; each must resolve via `name_search` against the corpus or it's dropped
|
| 202 |
+
(already how deep mode works). `choose()` reads the *result* pool, not the plan, for its gap signal.
|
| 203 |
+
- **Authority over-surfacing dead law** — the prior boosts high-cite_indeg cases, and overruled ex-landmarks are
|
| 204 |
+
high-cite_indeg. *Mitigation:* prior and good-law filter ship as one unit (proved: bad-law 0.173→0.000); never apart.
|
| 205 |
+
- **Budget spiral / spinner fatigue** — each move runs at most once; hard 15s cut; STOP returns on "no addressable gap."
|
| 206 |
+
- **Latency tax on easy queries** — round-1 STOP keeps clean doctrinal/known-item queries at ~2s; the loop only
|
| 207 |
+
spends rounds when the gap signal is real.
|
| 208 |
+
- **Self-grading creep** — eval judge family stays ≠ serving family; no LLM at score time; frozen qrels.
|
| 209 |
+
- **The loop can't ground** — if `confirmed` is off-topic the verbatim gate yields nothing; we render "couldn't
|
| 210 |
+
ground — review the cases" rather than a fluent hallucination (already the behaviour).
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## 9. Open founder decisions (verify before build)
|
| 215 |
+
|
| 216 |
+
1. **Latency:** soft 12s / hard 15s with progress — confirm. (Affects MAX rounds = 3.)
|
| 217 |
+
2. **Posture for the pilot:** conservative (precision-first: smaller `confirmed`, drop on any doubt) vs aggressive
|
| 218 |
+
(surface more authority, flag uncertainty). Recommend **conservative** for a lawyer's first impression.
|
| 219 |
+
3. **Concordance sourcing** (Stage 2): license a statute concordance vs hand-curate the BNS↔IPC core map.
|
| 220 |
+
4. **Endpoint cutover:** ship the agentic loop as the new default `search_stream` and keep old fast/deep as
|
| 221 |
+
fallbacks for one pilot cycle, or replace outright. Recommend **keep as fallback** through Hitin's pilot.
|
| 222 |
+
|
| 223 |
+
---
|
| 224 |
+
|
| 225 |
+
## 10. Code touch-points (for the build)
|
| 226 |
+
|
| 227 |
+
- **NEW** `scripts/agent.py` — brief, tools (thin wrappers over serve.py fns), `choose()` policy, the loop, STOP.
|
| 228 |
+
- `scripts/serve.py` — new adaptive `search_stream` driving `agent.py`; keep `deep_search_stream`/old fast as fallback.
|
| 229 |
+
- **NEW** `scripts/hyde.py` (or in agent.py) — draft-the-holding requery.
|
| 230 |
+
- **NEW** `scripts/build_statute_index.py` + `statute_lookup()` — Stage 2.
|
| 231 |
+
- **NEW** `eval/agentic_run.py` — runs the loop per query → run.tsv (the measurement substrate for every stage).
|
| 232 |
+
- **NEW** `eval/recall_recovery_{queries,qrels,badlaw}.tsv` — the recall-recovery slice.
|
| 233 |
+
</content>
|
| 234 |
+
</invoke>
|
phase1/CITATOR_DESIGN.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CP5 — good-law citator design (LOCKED)
|
| 2 |
+
|
| 3 |
+
*From the legal-agent debate (senior advocate · paralegal · citator architect). `good_law_status` is a **derived, cached projection** of inbound treatment edges — never an extraction, never inferred from silence. Default **`unknown`**. Asymmetric precision: near-abstention on `good_law` and on definitive `overruled`; recall of negatives routed into the conservative **`doubted`** sink. Every published badge carries provenance + a deep link a non-lawyer can clerically verify. No DeepSeek required for the verdict; the LLM (a **local Thor model**) only widens recall in Layer 2 and never certifies death.*
|
| 4 |
+
|
| 5 |
+
## Pipeline (recompute on ingest)
|
| 6 |
+
- **Layer 0 — edge resolution + nodes (deterministic).** Build the graph from `cases_cited`. Resolve each edge on **both** join keys — `neutral_citation` AND `equivalent_citations` (reporter cites), since pre-2023 cited-lists are reporter-only. **Legacy-only landmarks with no neutral citation (ADM Jabalpur, A.K. Gopalan, M.P. Sharma class) must become first-class reporter-cite-keyed vertices**, else their overruling edge dangles and the dead case silently reads `unknown`. Attach per edge: verbatim passage, pin-cite, citing+target date and bench_strength. Run the **self/reversal gate** here.
|
| 7 |
+
- **Layer 1 — express-declaration extraction (high-precision, GOLD).** Regex/(cheap-LLM) hunt for operative phrases naming a target: *"is/stands overruled · we overrule · no longer good law · does not lay down the correct law · overruled to the extent · declared per incuriam · hereby recalled."* The **only** thing allowed to auto-publish a clean negative. Replaces proprietary editorial flags (which are barred as IP liability).
|
| 8 |
+
- **Layer 2 — edge classification + deterministic gated aggregation (recall spine).** A **local Thor model** classifies each inbound edge → `{relied_on, referred_to, followed, distinguished, reaffirmed/approved, doubted/disapproved, overruled, per_incuriam-flag, referred_to_larger_bench}` + confidence + verbatim quote + pin-cite (default fail-safe `cited`). A **deterministic, auditable aggregator (never the LLM)** rolls edges through the gates. **Layer 2 alone never auto-publishes a clean `overruled`** — an uncorroborated model `overruled` is capped at `doubted` pending Layer-1 corroboration or review. Its job is to *manufacture doubt, not certify death.*
|
| 9 |
+
- **Layer 3 — statute supersession (Approach D, Phase-2, DARK).** `superseded_by_statute` only from a curated repeal/amendment+commencement table keyed on `sections` (never an LLM). Phase 1 ships it dark and instead shows a non-destructive **IPC/CrPC/IEA ↔ BNS/BNSS/BSA crosswalk banner** (BNS-class is prospective; pre-01-07-2024 offences keep the IPC line live; interpretive principles survive re-enactment).
|
| 10 |
+
- **Aggregation.** Final badge = highest-precedence non-null layer. Conflict = **worst-valid-status-wins** (doubt dominates affirmation; larger bench controls smaller). Approach C (holistic per-case LLM) is **barred from writing any state** — only a triage sort-key in the review queue. On every new judgment, recompute the status of every case it cites (staleness via `scraped_at`/`content_hash`).
|
| 11 |
+
|
| 12 |
+
## Gates (all deterministic)
|
| 13 |
+
- **Bench-strength** (both ends, fail-safe): `overruled/partly` valid only if `strength(citing) ≥ strength(target)` (single 1 < division 2 < full 3 < constitution 5 < larger 7). Either bench unknown → **downgrade to `doubted` + `bench-unverified`**, never `overruled`.
|
| 14 |
+
- **Temporal**: overruling/doubting edge must be strictly later than the target (tie-break on INSC number).
|
| 15 |
+
- **Court-hierarchy**: only SC may write negative status on an SC node; an `HC:<state>` edge can never overrule/suppress an SC target.
|
| 16 |
+
- **Ratio-vs-dicta / sub-silentio**: a negative counts only against the ratio the target is relied on for; disapproval of mere dicta or a silent conflict does not suppress.
|
| 17 |
+
- **Self/reversal**: *"reversed the impugned/HC judgment"* (appellate disposition, acts on parties) ≠ *"overruled"* (acts on a precedent) — never writes negative on cited precedents.
|
| 18 |
+
- **Confidence**: per-edge confidence below threshold → treated as neutral `cited`, contributes to no negative state.
|
| 19 |
+
- **Resolution/quorum**: conflicting inbound edges → worst valid status; larger-bench edge controls.
|
| 20 |
+
|
| 21 |
+
## Enum (trigger → guardrail)
|
| 22 |
+
- **`unknown`** (default) — no inbound negative, or negatives failed a gate, or low confidence, or thin coverage. Renders: *"No negative treatment found in N citing judgments — not yet human-reviewed; absence of flags is not a clearance,"* with provenance. **Code path for `good_law` must be structurally incapable of firing on silence** (unit test: a zero-edge node can only be `unknown`).
|
| 23 |
+
- **`good_law`** (narrow, Phase-1) — **positive assertion only**: ≥1 affirming edge (followed-and-approved/reaffirmed) by an equal-or-larger bench post-dating the case, zero unresolved negatives, bench known; names the affirming authority. Lights the 11 positive controls (Kesavananda, Maneka Gandhi, Minerva Mills, Puttaswamy). Bare `relied_on/followed` does **not** upgrade `unknown→good_law`.
|
| 24 |
+
- **`overruled`** — whole ratio displaced; Layer-1 express OR Layer-2 passing both gates AND corroborated (Layer-1 or dual-model). Auto-publishes only via Layer-1 express + gates + clean resolution; Layer-2-only → review queue, case holds prior state meanwhile.
|
| 25 |
+
- **`partly_overruled`** — severable part; scoped language; badge must carry surviving-vs-displaced text; **always human-reviewed** before it suppresses.
|
| 26 |
+
- **`doubted`** — the conservative fail-safe sink, deliberately over-inclusive (later doubt/disapproval without overruling; reference to a larger bench = `referred_to_larger_bench` sub-flag). **Auto-publishes freely** (only adds caution). `distinguished` does **not** land here (distinguished = still good law).
|
| 27 |
+
- **`per_incuriam`** — express-declaration ONLY (Layer-1 literal token naming the target). The classifier may detect but **never conclude** per incuriam by reasoning; guessed → `doubted` + "possible per incuriam" note.
|
| 28 |
+
|
| 29 |
+
## Auto-publish vs review
|
| 30 |
+
- **Tier-0 auto-publish (safe by construction):** `unknown`; `doubted`/`partly`-as-suppressor; and a negative with a **Layer-1 express verbatim sentence + named target + both gates + clean resolution** (the quote is its own auditor), or an edge in the human-signed gold set.
|
| 31 |
+
- **Everything else → review queue**, case holds its prior conservative state.
|
| 32 |
+
- Acceptance bar before any badge ships: `goodlaw_goldset.json` (21 scored + 11 positive controls) is both the auto-publish whitelist and the calibration target.
|
| 33 |
+
|
| 34 |
+
## Phase 1 vs later
|
| 35 |
+
- **Phase 1:** Layers 0–2 writing `{good_law (affirm-only, narrow), overruled, partly_overruled, doubted, unknown}` + all gates; `per_incuriam` via Layer-1 literal only; Tier-2 classifier = **local Thor model**.
|
| 36 |
+
- **Phase 2:** `superseded_by_statute` (curated table); proposition-scoped good-law; expanded review tooling.
|
| 37 |
+
|
| 38 |
+
## Open questions (need a qualified lawyer)
|
| 39 |
+
- per_incuriam competence rule (may a coordinate/smaller bench declare it?).
|
| 40 |
+
- Exact edge-pattern + bench conditions distinguishing per_incuriam vs superseded vs partly_overruled.
|
| 41 |
+
- Bounding surviving-vs-displaced text for `partly_overruled`.
|
| 42 |
+
- Final sign-off that narrow automated `good_law` is acceptable at launch.
|
| 43 |
+
- Confidence-cutoff calibration against the gold set.
|
phase1/HANDOFF.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Themis — Search-Relevance Handoff
|
| 2 |
+
|
| 3 |
+
> Owner handoff for whoever drives Themis search quality next. Covers: what the product is, **exactly how
|
| 4 |
+
> search works today (fast + deep), step by step**, the known weaknesses, and — most importantly — **the
|
| 5 |
+
> autoresearch methodology, the eval rig, the compute setup, and the experiment discipline** so you can run
|
| 6 |
+
> the propose→measure→commit loop yourself from day one. Read §4–§7 before touching any code.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 1. What Themis is & the goal
|
| 11 |
+
|
| 12 |
+
Grounded AI legal research over **37,898 reportable Indian Supreme Court judgments** (the open AWS registry
|
| 13 |
+
`indian-supreme-court-judgments`, ap-south-1). It retrieves on-point cases for a lawyer's query and produces a
|
| 14 |
+
grounded summary (every asserted point backed by a verbatim quote from a retrieved case).
|
| 15 |
+
|
| 16 |
+
**North star right now:** make *search quality* good enough for a live pilot with practising lawyers (via Hitin,
|
| 17 |
+
the lawyer co-founder). A blind benchmark vs CaseMine put us at nDCG@5 0.77 vs 0.64 — but it was a **3–3 split**
|
| 18 |
+
and **partly self-graded** (see §4), and the real, repeatedly-confirmed gap is **foundational-authority recall on
|
| 19 |
+
doctrinal queries** (landmark cases like Khushal Rao, Indra Sawhney rank too low or don't surface).
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 2. How search works TODAY — step by step
|
| 24 |
+
|
| 25 |
+
### 2.0 Serving stack
|
| 26 |
+
`phase1/scripts/serve.py` (FastAPI) + `frontend.html` (vanilla JS). The corpus is **chunked**: ~1,299,748
|
| 27 |
+
passages. Each chunk has a **BGE-small-en-v1.5** embedding (384-d, float32 matrix `M = escr_vectors.npy`) and
|
| 28 |
+
sits in a **BM25** index. The serving LLM is **DeepSeek** (`deepseek-chat`) via `~/.../.env` `DEEPSEEK_API_KEY`.
|
| 29 |
+
Artifacts: `escr_chunks.jsonl` (chunk text), `escr_vectors.npy`, `escr_meta.jsonl` (per-judgment metadata incl.
|
| 30 |
+
`issue`/`held` headnotes), `edges.jsonl` (citation graph, 86,702 edges), `good_law.jsonl` (citator), `escr_pdfmap.jsonl`.
|
| 31 |
+
|
| 32 |
+
### Shared retrieval primitives
|
| 33 |
+
- **`dense(q)`** — embed `"Represent this sentence for searching relevant passages: " + q` with BGE-small, cosine
|
| 34 |
+
`M @ qv`, take **top CAND=40 chunks**.
|
| 35 |
+
- **`bm25_top(q)`** — BM25 over tokenized chunks, top 40. ⚠ `rank_bm25.get_scores` scans 1.3M postings in pure
|
| 36 |
+
Python = **~68 s/query** — the reason the eval can't use the full serve path (see §6).
|
| 37 |
+
- **`candidates(q)`** — **RRF-fuse** dense+BM25 (k=60), top ~48 chunk candidates.
|
| 38 |
+
- **`rerank(q,cand,k)`** — cross-encoder **ms-marco-MiniLM-L-6-v2** scores `(q, chunk)`, dedups to **best chunk per
|
| 39 |
+
judgment**, returns top-k docs (`rr` = cross-encoder score).
|
| 40 |
+
- **`verify(q,results)`** — a DeepSeek "paralegal" labels each result `relevant/partial/not`, seeing only a
|
| 41 |
+
~280-char `passage_snippet` (NOT the holding). On JSON parse failure → everything defaults to `partial`.
|
| 42 |
+
- **Grounding gate** (`grounded_answer`/`verify_claims`) — DeepSeek emits `{claim, n, quote}` triples; the gate drops
|
| 43 |
+
any claim whose `quote` isn't a ≥4-word verbatim substring of cited case `n`'s chunk; renders only survivors.
|
| 44 |
+
(Checks provenance, **not entailment** — a real quote can be misread; that's a known residual.)
|
| 45 |
+
- **Good-law mask** is "dark": only confirmed-overruled is flagged; unknown shows nothing.
|
| 46 |
+
|
| 47 |
+
### FAST mode — `GET /api/search_stream` (SSE, stepwise)
|
| 48 |
+
- **F0 ROUTER** `identity_hits(q)` — if q is a bare **citation** (regex) or **"X v Y" name** (fuzzy `name_search`,
|
| 49 |
+
difflib + `cite_indeg` salience tiebreak), it's a *lookup* → metadata `id_card`, **bypass** the pipeline.
|
| 50 |
+
- **F1 RETRIEVE** `candidates(q)` → ~48 hybrid chunks.
|
| 51 |
+
- **F2 RERANK** cross-encoder → top 12 judgments.
|
| 52 |
+
- **F3 REVIEW** `verify` → keep relevant+partial, drop `not`.
|
| 53 |
+
- **F4 THIN-GUARD** if kept<4 → one DeepSeek query rewrite → re-retrieve/rerank/verify, add new.
|
| 54 |
+
- **F5 ORDER** sort relevant-first, then by `rr`; top 8.
|
| 55 |
+
- **F6 GOOD-LAW** dark mask. ⚠ **In fast mode this is currently a no-op** — the SSE label says "Checked which
|
| 56 |
+
results are still good law" but NO fast-path code filters/demotes overruled cases (deep mode does). Fix pending.
|
| 57 |
+
- **F7 ANSWER** grounded summary (claims + verbatim-quote gate).
|
| 58 |
+
|
| 59 |
+
### DEEP mode — `GET /api/deep_search_stream`
|
| 60 |
+
- **D0 ROUTER** same identity bypass.
|
| 61 |
+
- **D1 PLAN** DeepSeek decomposes the issue into 1–3 sub-issues + **names ≤5 leading authorities** a lawyer expects
|
| 62 |
+
(names only; each grounded via `name_search` — a hallucinated name simply fails to resolve).
|
| 63 |
+
- **D2 SEED** `retrieve(q,12)` then `verify` → relevant/partial seed set.
|
| 64 |
+
- **D3 EXPAND** (the only agentic step, one batch): (a) ground each named authority via `name_search`; (b) **citation
|
| 65 |
+
neighbours** — from top-6 seed docs, pull what THEY cite (`out_edges`), take 6 most-common; (c) `card_for_doc` per
|
| 66 |
+
added doc (reranks **only its first 6 chunks — `cis[:6]`, a real bug**: a landmark's holding is often deeper → it
|
| 67 |
+
gets mis-scored and dropped) → verify → keep relevant/partial AND not-overruled.
|
| 68 |
+
- **D4 AUTHORITY-CHECK** report which named authorities landed (conflates "found but off-point" vs "not in corpus").
|
| 69 |
+
- **D5 RANK** a reviewer-confirmed PLAN authority that's `relevant` gets a top slot; then relevant by `rr`; then
|
| 70 |
+
partials; top 10. ⚠ authority bonus is **binary + AND-gated on the snippet-derived `relevant` label**.
|
| 71 |
+
- **D6 GOOD-LAW + ANSWER** same as fast.
|
| 72 |
+
|
| 73 |
+
### Other endpoints
|
| 74 |
+
`/api/judgment?id=` (full judgment view — metadata, issue/held headnote, citator, dark good-law, cleaned text, +
|
| 75 |
+
**`/api/pdf?id=`** which pulls the official SCR PDF from the open registry and caches ≤20 locally, embedded inline),
|
| 76 |
+
`/api/search` (non-stream fallback). The "ask this judgment" feature was removed.
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## 3. Known weaknesses (panel-reviewed, grounded in serve.py) — prioritized
|
| 81 |
+
|
| 82 |
+
The foundational-recall gap is **created in F1 (shallow 48-chunk pool)**, never recovered because **ranking ignores
|
| 83 |
+
authority (F5/D5 use raw `rr`; `cite_indeg`/`bench`/`date` sit on every card unused)**, and actively *worsened* by
|
| 84 |
+
**F3 deleting weakly-phrased landmarks on a 280-char snippet** and **D3's `cis[:6]` bug**. Plus the **F6 fast-mode
|
| 85 |
+
good-law no-op** (a trust defect: an overruled case can rank #1 under a false "checked" label; only ~40 of 43,175
|
| 86 |
+
docs are ever flaggable, `partly_overruled` in the D3 filter is dead code, `doubted` is wrongly omitted). Both
|
| 87 |
+
models (BGE-small embedder, ms-marco reranker) are **general web English, not legal/India-tuned**.
|
| 88 |
+
|
| 89 |
+
Full per-step verdicts + the prioritized sequence are in the memory file index and the panel transcripts (§9).
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 4. The AUTORESEARCH approach (the methodology — read this)
|
| 94 |
+
|
| 95 |
+
Modeled on **Karpathy's AutoResearch** (Mar 2026): an agent runs experiments in a loop — *read code → propose ONE
|
| 96 |
+
change → run a short job → measure ONE mechanical metric → `git commit` if it improved / `git revert` if not →
|
| 97 |
+
repeat.* Three pillars: **(1) a hard constraint, (2) one mechanical metric, (3) autonomous propose/score/commit.**
|
| 98 |
+
Plus his older "Recipe" discipline: become one with the data first, dumb baseline, **change ONE thing at a time,
|
| 99 |
+
never add unverified complexity.**
|
| 100 |
+
|
| 101 |
+
How we adapt it (the rules — do not break these):
|
| 102 |
+
- **The metric is a FROZEN qrels file scored by pure arithmetic — NO LLM at score time.** This makes it
|
| 103 |
+
millisecond-cheap to recompute and **non-self-gradeable by construction**.
|
| 104 |
+
- **The judge must NEVER be the serving model family.** Our live `verify` gate is DeepSeek, so DeepSeek is *banned*
|
| 105 |
+
from labeling the eval (that's why the old 0.77 number is inadmissible — `20_score_benchmark.py` graded
|
| 106 |
+
DeepSeek-with-DeepSeek). Labels came from **Claude** (a different family); **Codex** is the cross-family second
|
| 107 |
+
judge; the citation graph + known-item are model-free anchors.
|
| 108 |
+
- **Commit a change only if its metric delta clears the paired-bootstrap 95% CI AND no guardrail regresses.**
|
| 109 |
+
- **Cheap vs expensive experiments:** anything that only re-ranks (pool depth, authority α, reranker swap, the
|
| 110 |
+
ordering rule) is a ~65 s offline re-score → run hundreds. Anything that **re-embeds the 1.3M corpus** (swapping
|
| 111 |
+
the *embedder*) is expensive and gated behind a proven "landmarks enter the pool but rank low" signal.
|
| 112 |
+
- **Precision-first:** a wrong overruled-case shown to a lawyer is the catastrophic error → the **bad-law@10**
|
| 113 |
+
guardrail and **known-item success@1** can never regress for a commit to count.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## 5. The EVAL RIG (the measurement substrate) — `phase1/eval/`
|
| 118 |
+
|
| 119 |
+
**Frozen set: 800 queries** (`queries.tsv` + `qrels.tsv` + `bad_law_docids.txt`):
|
| 120 |
+
- **500 SILVER** doctrinal / fact-pattern / vague — generated by **Claude subagents** (a Workflow, 25 agents) that
|
| 121 |
+
read a judgment's `held`/`issue` headnote and wrote 2 natural, **anti-leak** queries it answers (NEVER naming the
|
| 122 |
+
case/citation); gold = that source case (grade 3). `gen_sample.json` = the 250 sampled judgments. This is
|
| 123 |
+
**silver** → to be **audited by Hitin** (double-label a stratified ~200 sample, compute Cohen's κ, promote to
|
| 124 |
+
gold). The Claude+Codex dual-judge for graded multi-relevant labels (pooling top-k from fast/deep/BM25) is the
|
| 125 |
+
next eval upgrade.
|
| 126 |
+
- **300 known-item** (150 neutral-citation + 150 case-name) — gold = own doc; the **success@1 control**.
|
| 127 |
+
- **104 bad-law deny-list** (overruled/doubted/per_incuriam doc_ids) — the model-free **precision guardrail**.
|
| 128 |
+
|
| 129 |
+
> ⚠ **Critical caveat:** the silver gold = a *random* source case, not a landmark → **this set measures GENERAL
|
| 130 |
+
> retrieval, NOT foundational-authority recall**. The authority prior (PageRank/`cite_indeg`) must be tested on a
|
| 131 |
+
> **landmark set** (extend `eval/gold_foundational.json`), where gold IS a landmark — on the silver set it
|
| 132 |
+
> *catastrophically hurts* (see §7). Building a bigger landmark/foundational gold set is a top eval to-do.
|
| 133 |
+
|
| 134 |
+
**Metric suite** (`score_qrels.py`, pure numpy, frozen, no network):
|
| 135 |
+
- **PRIMARY: nDCG@10** (graded, 2^g−1 gain) + nDCG@5.
|
| 136 |
+
- **GUARDRAILS: known-item success@1** (must stay ~1 once the router is in the harness) and **bad-law@10** (lower is
|
| 137 |
+
better; a commit that raises it is auto-rejected).
|
| 138 |
+
- **DIAGNOSTICS:** recall@10, MAP@20, MRR, per-intent breakdown.
|
| 139 |
+
- All with **bootstrap-over-queries 95% CIs**.
|
| 140 |
+
|
| 141 |
+
**Harnesses:**
|
| 142 |
+
- `lean_run.py` — serial, dense+CE, drops BM25 (the 68s killer), env knobs. ~0.74 s/q.
|
| 143 |
+
- `batched_run.py` — **GPU-batched** (encode all queries → one dense matmul → ONE batched cross-encoder pass) →
|
| 144 |
+
**full 800-query run in ~65 s** (~9× the serial). This is the loop harness.
|
| 145 |
+
- `sweep.py` — loads the corpus once and scores many (reranker × CAND × ALPHA) configs with paired bootstrap vs
|
| 146 |
+
baseline. The autonomous experiment driver.
|
| 147 |
+
- `embed_chunks.py` — re-embed the corpus on GPU (exists but **too slow** ~80 min — transfer original vectors instead).
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
## 6. COMPUTE — where & how to run
|
| 152 |
+
|
| 153 |
+
### The Windows GPU box (the experiment machine)
|
| 154 |
+
`ssh admin@100.81.98.43` (tailnet). **RTX 5060 Ti 16 GB (Blackwell sm_120), 62 GB RAM, Python 3.11, default shell
|
| 155 |
+
PowerShell.** App dir `C:\Users\admin\themis`. Setup gotchas you WILL hit:
|
| 156 |
+
- My key is in `C:\ProgramData\ssh\administrators_authorized_keys` with an `icacls` perms lock — **required** or
|
| 157 |
+
sshd silently ignores it (admin accounts).
|
| 158 |
+
- **torch must be `+cu128`** for Blackwell. The venv's old pip backtracks to the CPU wheel — `pip install -U pip`
|
| 159 |
+
first, then `pip install torch --index-url https://download.pytorch.org/whl/cu128` (NOT `--extra-index-url`,
|
| 160 |
+
which re-picks the CPU build). Current: `torch 2.11.0+cu128`, CUDA True.
|
| 161 |
+
- **`PYTHONUTF8=1` is MANDATORY** — Windows `open()` defaults to cp1252 → `UnicodeDecodeError` on the legal text.
|
| 162 |
+
- Box is set to never-sleep + `tailscale up --unattended` so it stays reachable logged-out.
|
| 163 |
+
|
| 164 |
+
**Run an experiment (the loop in practice):**
|
| 165 |
+
```powershell
|
| 166 |
+
cd $env:USERPROFILE\themis
|
| 167 |
+
$env:PYTHONUTF8="1"; $env:THEMIS_DATA="."; $env:THEMIS_EVAL="."; $env:THEMIS_DEVICE="cuda"
|
| 168 |
+
# one config:
|
| 169 |
+
$env:CAND="40"; $env:ALPHA="0"; $env:THEMIS_RERANKER="cross-encoder/ms-marco-MiniLM-L-6-v2"
|
| 170 |
+
.\venv\Scripts\python.exe batched_run.py # -> run.tsv (~65s)
|
| 171 |
+
.\venv\Scripts\python.exe score_qrels.py run.tsv
|
| 172 |
+
# or sweep many at once:
|
| 173 |
+
.\venv\Scripts\python.exe sweep.py
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
### Getting artifacts onto the box (it was painful — documented so you don't repeat it)
|
| 177 |
+
Mac→box tailnet is DERP-relayed (~0.8 MB/s, useless). Mac can't reach Thor from its network. **Thor** (the old
|
| 178 |
+
Jetson, now `100.99.130.27` tailnet, **flaky**) shares a LAN with the box → the box pulls Thor:`~/backup` over LAN
|
| 179 |
+
(`192.168.1.76`, ~3–5 MB/s). We added the box's own SSH key to Thor (from the Mac, retrying through Thor's
|
| 180 |
+
flakiness). **Re-embedding on the 5060 Ti is too slow (~80 min, CPU-tokenization-bound) — transferring the original
|
| 181 |
+
`escr_vectors.npy` (~10 min) beats it and is exact.**
|
| 182 |
+
|
| 183 |
+
### Other places it runs
|
| 184 |
+
- **Local serve (the live app, for demoing/QA):** on the Mac, `.venv` (py3.12) + torch CPU, launched from the
|
| 185 |
+
artifacts dir: `THEMIS_LOG_DIR=… .venv/bin/uvicorn --app-dir phase1/scripts serve:app --host 127.0.0.1 --port 8000`
|
| 186 |
+
(artifacts opened CWD-relative; `scripts/.env` holds the DeepSeek key; no passcode on localhost).
|
| 187 |
+
- **Pilot deploy:** `phase1/deploy/` has a ready Caddy + systemd + runbook for a Hetzner CPX41 (Caddy auto-TLS →
|
| 188 |
+
uvicorn + a passcode gate, `themis.apexflo.ai`). Serving is CPU-only — no GPU needed to host. Not yet provisioned.
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## 7. Results so far (the experiment log — keep appending to this)
|
| 193 |
+
|
| 194 |
+
| Config | nDCG@10 | recall@10 | succ@1 (known) | bad-law@10 | Δ vs baseline (paired bootstrap) | Verdict |
|
| 195 |
+
|---|---|---|---|---|---|---|
|
| 196 |
+
| **ms-marco, CAND=40, α=0** (baseline) | **0.517** | 0.595 | 0.31 | 0.037 | — | baseline |
|
| 197 |
+
| ms-marco, CAND=100, α=0 | 0.532 | 0.611 | 0.34 | 0.036 | **+0.015 [+0.005, +0.026] ✓sig** | **COMMIT** (deeper pool helps) |
|
| 198 |
+
| ms-marco, CAND=40, α=0.5 | 0.231 | 0.495 | 0.02 | 0.102 | −0.286 [−0.309, −0.262] | **REJECT** (authority prior wrong on silver) |
|
| 199 |
+
| ms-marco, CAND=100, α=0.5 | 0.131 | 0.299 | 0.00 | 0.179 | −0.386 | **REJECT** |
|
| 200 |
+
|
| 201 |
+
### 7b. The AUTHORITY slice — `authority_{queries,qrels,badlaw}` (150 landmark doctrinal queries)
|
| 202 |
+
|
| 203 |
+
Built to make the rig *see* the foundational-authority gap the silver set hides (silver gold is the random
|
| 204 |
+
source-case, not the landmark; so it actively penalizes authority). Gold = the doctrine's landmark (grade 3) +
|
| 205 |
+
strong-citation progeny (grade 2). Run with `THEMIS_QFILE=authority_queries.tsv THEMIS_QRELS=authority_qrels.tsv
|
| 206 |
+
THEMIS_QUERIES=authority_queries.tsv THEMIS_BADLAW=authority_badlaw.txt`.
|
| 207 |
+
|
| 208 |
+
| Config | nDCG@10 | nDCG@5 | MRR | recall@10 | bad-law@10 | Verdict |
|
| 209 |
+
|---|---|---|---|---|---|---|
|
| 210 |
+
| **ms-marco, α=0** (baseline) | **0.282** [.253,.312] | — | 0.551 | 0.204 | 0.113 | baseline — the gap, quantified (vs 0.517 silver) |
|
| 211 |
+
| bge-reranker-base, α=0 | 0.283 [.251,.313] | 0.315 | 0.553 | 0.195 | 0.127 | **REJECT** — Δ+0.001, a better topical reranker does NOT find foundational law |
|
| 212 |
+
| ms-marco, α=0.3 | 0.354 [.324,.385] | — | 0.692 | 0.195 | 0.173 | win on nDCG but **bad-law regresses** (boosts overruled ex-landmarks) |
|
| 213 |
+
| ms-marco, α=0.6 | 0.353 | — | 0.686 | 0.194 | 0.180 | α plateaus past 0.3 |
|
| 214 |
+
| **ms-marco, α=0.3 + good-law filter** | **0.351** [.320,.384] | **0.416** | **0.693** | 0.193 | **0.000** | **COMMIT** — full nDCG gain held, bad-law → 0 |
|
| 215 |
+
|
| 216 |
+
Reading — three decisive results:
|
| 217 |
+
1. **Reranker swap is dead.** bge ≈ ms-marco (+0.001) on the exact slice it was meant to fix. Topical rerankers
|
| 218 |
+
under-score old-language landmarks regardless of model. Don't ship the 1.1GB model / 10× slower CPU pass.
|
| 219 |
+
2. **The authority prior is the lever:** +0.072 nDCG@10 (+24% rel), MRR 0.551→0.692; recall@10 flat → it's a pure
|
| 220 |
+
**ranking** fix (landmarks were always in-pool, just low) — exactly the diagnosis the pool-probe gave.
|
| 221 |
+
3. **It ships gated by good-law:** authority alone lifts bad-law (0.113→0.173); the denylist filter holds the gain
|
| 222 |
+
(0.351) and drops bad-law to 0.000. **Authority prior + good-law filter are one unit, never shipped apart.**
|
| 223 |
+
|
| 224 |
+
The architecture consequence: the prior is **−0.29 on general silver, +0.072 on doctrinal** → it must be
|
| 225 |
+
**query-routed** (ON for doctrinal/principle-seeking, OFF for known-item/fact lookup). That swing is the empirical
|
| 226 |
+
mandate for a cheap LLM query-classifier at the front. Caveats: silver labels (Hitin audit pending); mild
|
| 227 |
+
circularity (slice gold & prior both key off `cite_indeg` → magnitude may inflate, direction is sound); bad-law→0
|
| 228 |
+
is only as real as the denylist, so it depends on F6 good-law being real in production.
|
| 229 |
+
|
| 230 |
+
### 7c. STAGE 0 of the agentic build — routed authority prior + good-law spine (committed 2026-06-25)
|
| 231 |
+
|
| 232 |
+
First stage of `AGENTIC_SEARCH_SPEC.md`. Folds the §7b lever into one *query-routed* spine and measures it
|
| 233 |
+
end-to-end. Round-0 intent classifier (`classify_intent.py`, DeepSeek: AUTHORITY vs SPECIFIC) gates the prior;
|
| 234 |
+
good-law filter drops the 43 confirmed-bad docs (`goodlaw_badlaw.txt`, the real product signal — not the ad-hoc
|
| 235 |
+
`authority_badlaw.txt`). Harness: `spine_run.py` (loads corpus once, CE once/slice, derives baseline + spine).
|
| 236 |
+
Routing fires on **69%** of authority queries, **6%** of silver — aggressive-OFF on general, as designed.
|
| 237 |
+
|
| 238 |
+
| Slice | paired ΔnDCG@10 vs baseline | Δsucc@1 | bad-law@10 |
|
| 239 |
+
|---|---|---|---|
|
| 240 |
+
| **AUTHORITY (150)** | **+0.048 [+0.028, +0.070] SIG** | **+0.12 SIG** | 0.033 → **0.000** |
|
| 241 |
+
| SILVER all-800 | −0.013 [−0.021, −0.006] SIG | −0.020 | 0.013 → **0.000** |
|
| 242 |
+
|
| 243 |
+
The silver regression is **confined to `silver_doctrinal` (−0.036 SIG); factpattern, vague, known-item are all
|
| 244 |
+
flat/ns.** It is an eval artifact: silver_doctrinal gold is the *arbitrary source case*, not the doctrine's
|
| 245 |
+
landmark, so it penalizes us for correctly surfacing leading authority — the **same query type scores +0.048 on
|
| 246 |
+
the authority slice where the gold is the landmark.** The real (correctly-specified) guardrails — known-item
|
| 247 |
+
success@1, factpattern, vague, bad-law — all hold. **Verdict: PASS**; proceed to Stage 1.
|
| 248 |
+
|
| 249 |
+
Open caveats logged for Hitin's audit: (a) classifier precision — it over-fires AUTHORITY on a few *narrow*
|
| 250 |
+
doctrinal queries ("frustration in a statutory tenancy") where a specific case may beat the landmark; correct
|
| 251 |
+
gold is needed to tune this. (b) good-law **coverage** — only 43 docs flagged bad in 38k; the filter is perfect
|
| 252 |
+
within what's labeled but labeling is thin (the F6 dependency). (c) silver_doctrinal needs landmark gold to be a
|
| 253 |
+
valid instrument for authority features.
|
| 254 |
+
|
| 255 |
+
Files: `classify_intent.py`, `spine_run.py`, `goodlaw_badlaw.txt`, `intent_{authority,silver}.json`,
|
| 256 |
+
`{authority,silver}_{baseline,spine}.tsv`.
|
| 257 |
+
|
| 258 |
+
### 7d. STAGE 1 — the agentic controller (two-turn parallel) — PASS (2026-06-26)
|
| 259 |
+
|
| 260 |
+
Tool-rich ReAct agent (founder-chosen over the binary router), shaped by the panel as **two-turn parallel**,
|
| 261 |
+
not serial N-step ReAct: 1 plan LLM call (intent + expected authorities + statute refs + HyDE) → ALL retrieval
|
| 262 |
+
tools fan out at once (`vector` + `authority` + `name_lookup`(plan authorities) + `statute`/`cases_on_section`
|
| 263 |
+
+ `hyde` + citation-`graph`) → merge → one batched CE rerank → authority-prior rank → good-law **flag-don't-drop
|
| 264 |
+
-for-authority**. Files: `scripts/tools.py` (14-tool registry), `scripts/agent.py` (controller), `eval/agentic_run.py`.
|
| 265 |
+
|
| 266 |
+
| Authority slice (150) | nDCG@10 | MRR | succ@1 | recall@10 | bad-law@10 |
|
| 267 |
+
|---|---|---|---|---|---|
|
| 268 |
+
| baseline | 0.282 | 0.551 | 0.380 | 0.204 | 0.033 |
|
| 269 |
+
| spine (Stage 0) | 0.330 | 0.638 | 0.500 | 0.194 | 0.000 |
|
| 270 |
+
| **agent** | **0.389** | **0.788** | **0.687** | 0.181 | 0.160 |
|
| 271 |
+
|
| 272 |
+
- **agent vs spine paired ΔnDCG@10 = +0.0585 [+0.028,+0.089] SIG**; Δrecall@10 ns (no real loss). The controlling
|
| 273 |
+
authority is #1 in **69%** of queries (succ@1 0.500→0.687).
|
| 274 |
+
- **Recall-recovery slice (8 landmarks OUTSIDE the dense pool): recall@10 = 0.75** vs spine ≈ 0 — the agent
|
| 275 |
+
(name_lookup of LLM-named authorities + graph + hyde) solves what one-shot retrieval structurally cannot.
|
| 276 |
+
- **bad-law@10 0.160 is 88% artifact:** 23/26 hits are high-authority FALSE-POSITIVE landmarks (Maneka-type)
|
| 277 |
+
that flag-don't-drop keeps WITH A WARNING; only 3 are genuine dead law (≈ spine's leak). The headline rise is
|
| 278 |
+
the good-law data's mislabels, not a precision regression. **→ Hitin's good-law audit is now the #1 data item.**
|
| 279 |
+
|
| 280 |
+
Latency: the Mac eval is ~11s/query (CPU, multiple sequential CE passes) — production (GPU box, async parallel
|
| 281 |
+
tools) hits the panel's 15s budget. `name_lookup` indexed (token postings) so it's no longer O(corpus).
|
| 282 |
+
NEXT: product path (2nd LLM turn = judge + streamed grounded answer + SSE steps), wire as serve.py adaptive
|
| 283 |
+
endpoint (keep fast/deep as fallback), per-tool ablation (cut tools that don't earn latency), good-law audit.
|
| 284 |
+
|
| 285 |
+
---
|
| 286 |
+
|
| 287 |
+
## 8. The stepwise plan (roadmap — panel-prioritized)
|
| 288 |
+
|
| 289 |
+
- **Step 0 — eval rig** ✅ (this doc / §5).
|
| 290 |
+
- **Step 1 — cheap honesty/correctness** (ship together, low risk): make F6 good-law **real in fast mode** (demote/
|
| 291 |
+
tag the ~40 confirmed-overruled), fix the D3 good-law token set (drop `partly_overruled`, add `doubted`), log the
|
| 292 |
+
`verify` parse-failure no-op, sigmoid-normalize `rr`, share ONE F0/D0 router fn, emit D4 got/miss to the usage log.
|
| 293 |
+
- **Step 2 — pool depth** (CAND↑ + diversity) — first signal positive (+0.015).
|
| 294 |
+
- **Step 3 — ranking core (now data-backed, §7b):** ship the **authority prior** `sigmoid(rr)+α·log1p(cite_indeg)`
|
| 295 |
+
at **α=0.3**, **query-routed** (ON for doctrinal/principle queries only — it's −0.29 on general, +0.072 on
|
| 296 |
+
doctrinal) and **gated by the good-law filter** (the two are one unit). Needs (a) a cheap LLM query-classifier at
|
| 297 |
+
the front, (b) F6 good-law real. headnote-fed `verify` (down-weight not drop high-authority), fix `card_for_doc
|
| 298 |
+
cis[:6]`. **NOTE: the bge-reranker swap is REJECTED (§7b) — do not pursue; the reranker is not the lever.**
|
| 299 |
+
- **Step 4 — router guards** (question-word before bypass; bare-name lookup) + A/B-and-maybe-cut the F4 rewrite.
|
| 300 |
+
- **Step 5 — answer:** deep synthesis over top-8 + force-include confirmed authorities + a Qwen/cross-family
|
| 301 |
+
**entailment** gate after the verbatim gate.
|
| 302 |
+
- **Stage 2 (post-pilot, measured):** swap to a **legal/Indic embedder** (needs a re-embed of the corpus —
|
| 303 |
+
expensive; note the **bge-reranker is already ruled out by §7b**, so this is an *embedder* bet, not a reranker one); the **citation-resolution rebuild** (it's a MISSING-KEYS problem — SCC resolves at 0% — not
|
| 304 |
+
name-matching; tiered exact-lookup spine + trust firewall); the **statute layer (BNS↔IPC / BNSS↔CrPC / BSA↔Evidence)
|
| 305 |
+
+ unified intent→plan→tools** agentic search (collapse fast/deep into one adaptive-effort pipeline);
|
| 306 |
+
**PageRank / Personalized PageRank** for authority (gated on clean Tier-1/2 edges).
|
| 307 |
+
|
| 308 |
+
---
|
| 309 |
+
|
| 310 |
+
## 9. Rigor principles (the culture to keep)
|
| 311 |
+
|
| 312 |
+
- **Verify against the real code/data — the panels repeatedly caught wrong assumptions** (e.g., "registry has SCC
|
| 313 |
+
cites" was false; "re-scrape the registry" buys 0 keys; the "76 overruled" figure was wrong — it's 40). Read the
|
| 314 |
+
file before you claim.
|
| 315 |
+
- **Never self-grade.** Judge family ≠ serving family. Anchor in model-free truth (citation graph, known-item) where
|
| 316 |
+
possible; reserve LLMs for the gaps and audit them.
|
| 317 |
+
- **One change at a time → paired-bootstrap CI → commit or revert.** No unverified complexity.
|
| 318 |
+
- **Precision-first guardrails are non-negotiable** (bad-law@10, known-item succ@1).
|
| 319 |
+
- **Reproducibility/backups:** artifacts live on the Mac (`phase1/data/thor_artifacts/`, byte-verified), Thor
|
| 320 |
+
`~/backup`, and the GPU box; code on GitHub. The eval set + qrels are frozen and versioned.
|
| 321 |
+
|
| 322 |
+
---
|
| 323 |
+
|
| 324 |
+
## 10. Pointers
|
| 325 |
+
|
| 326 |
+
- Code: `phase1/scripts/serve.py`, `frontend.html`; eval: `phase1/eval/{build_qrels,score_qrels,lean_run,batched_run,sweep,embed_chunks}.py` + `queries.tsv`/`qrels.tsv`/`bad_law_docids.txt`; deploy: `phase1/deploy/`.
|
| 327 |
+
- Design records (in the user's auto-memory): **themis-eval-rig**, **themis-citation-resolution**, **themis-agentic-search-design**, **themis-benchmark-v1**, **thor-gpu-embedding** (the box saga + gotchas).
|
| 328 |
+
- The panel reviews (per-step search critique, eval-metric design, citation-resolution) ran as multi-agent Workflows; their full transcripts are in the session's `subagents/workflows/` dirs.
|
| 329 |
+
|
| 330 |
+
**First thing to do when you pick this up:** ssh the GPU box, run `sweep.py`, read the bge-reranker-base row, and
|
| 331 |
+
either commit it (if it clears the CI and guardrails hold) or move to Step 1. The loop is live — turn the crank.
|
phase1/INDIAN_KANOON_MIGRATION.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Indian Kanoon corpus migration contract
|
| 2 |
+
|
| 3 |
+
The case page and case chat are source-agnostic. They consume stable artifact contracts rather than
|
| 4 |
+
AWS or Indian Kanoon URLs, so the frontend does not need another rewrite when the corpus changes.
|
| 5 |
+
|
| 6 |
+
## Extraction output
|
| 7 |
+
|
| 8 |
+
Keep `doc_id` stable across every artifact. In addition to metadata, paragraph-aware judgment text,
|
| 9 |
+
source provenance, and embeddings, produce:
|
| 10 |
+
|
| 11 |
+
`judgment_summaries.jsonl`
|
| 12 |
+
|
| 13 |
+
```json
|
| 14 |
+
{
|
| 15 |
+
"doc_id": "2024 INSC 123",
|
| 16 |
+
"summary": "A neutral 150–400 word case summary grounded only in the extracted judgment.",
|
| 17 |
+
"provider": "indian_kanoon",
|
| 18 |
+
"version": "ik-extraction-v1",
|
| 19 |
+
"generated": true
|
| 20 |
+
}
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
`summary` may alternatively be a structured object with `facts`, `issues`, `holding`, `reasoning`,
|
| 24 |
+
and `outcome`. The serving layer normalizes either form. Case chat receives only this normalized
|
| 25 |
+
summary, never the full judgment or retrieval corpus.
|
| 26 |
+
|
| 27 |
+
## Corpus-repair parity gate
|
| 28 |
+
|
| 29 |
+
Before cutover, rebuild and validate every repair described in `themis-audits/corpus_repair.html`:
|
| 30 |
+
|
| 31 |
+
- identity ledger, decision-year correction, text health, and sibling canonicals;
|
| 32 |
+
- famous-name aliases with the doctrinal-query residue guard;
|
| 33 |
+
- body-text citation graph and guarded parallel-citation crosswalk;
|
| 34 |
+
- citation-context and HELD/headnote retrieval representations;
|
| 35 |
+
- synthetic headnotes only where extracted summaries/headnotes remain missing;
|
| 36 |
+
- edge treatment classification and rolled-up good-law status;
|
| 37 |
+
- paragraph-aware chunks, pin-cite identifiers, FTS, dense vectors, and evaluation qrels.
|
| 38 |
+
|
| 39 |
+
Run the same known-item, doctrine, factual, statute, bad-law, PDF/source, and lawyer-gold evaluation
|
| 40 |
+
gates before switching the backend artifact revision. The UI will automatically prefer the new
|
| 41 |
+
extraction summary and disclose its provenance.
|
phase1/METADATA_SCHEMA_CP2.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CP2 — Themis canonical per-judgment metadata schema (LOCKED)
|
| 2 |
+
|
| 3 |
+
*Converged via a 3-persona debate (paralegal / senior advocate / data-feasibility) + adversarial cross-examination + presiding synthesis. Claims were cross-checked against the on-disk 3k SC corpus. Anchored on the existing `JudgmentMetadata` (themisV2) + the V2 Postgres `case` table.*
|
| 4 |
+
|
| 5 |
+
> LOCKED: Themis CP2 canonical per-judgment schema — 27 fields. The schema's job is threefold: pin the RIGHT case, weigh its AUTHORITY, and assert GOOD LAW only when an edge proves it. Two non-obvious but decisive locks, both confirmed against the on-disk 3k corpus: (1) equivalent_citations is a P0 LIST and the citator's second join key — INSC appears in only 0.9% of corpus and the good-law gold set is 100% reporter-keyed, so a neutral-citation-only graph cannot resolve its own ground truth or any pre-2023 landmark; (2) good_law_status is a derived, cached projection that DEFAULTS to 'unknown' (never 'good_law') with a 7-state enum, and bench_strength is a correctness gate read on both edge ends that FAILS SAFE — verified bench parses on only 54.5% of corpus, so a missing-coram overruling is downgraded to doubted/unknown, never validated. disposition is split from good-law as a closed enum (corpus shows real 'disposed'/'disposed of' free-text noise). HELD and issue are P1, verbatim-where-present, null-never-fabricated (HELD marker 3.8%, issue 0.2% off-SCR). advocates and topic_path taxonomy are deferred — zero extraction budget while the treatment-classification moat is unfinished.
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
## P0 fields
|
| 9 |
+
|
| 10 |
+
| field | roles | extractability | improves | source | notes |
|
| 11 |
+
|---|---|---|---|---|---|
|
| 12 |
+
| `acts (normalized list of canonical act IDs)` | filter/retrieval/citator | reliable | filter UX + known-item retrieval | SCR 'List of Acts' + body via act gazetteer; maintained alias table + IPC/CrPC/IEA<->BNS/BNSS/BSA crosswalk | Top-3 daily statute-anchored intent and the bridge to the statute layer. MUST normalize to canonical IDs and carry the old<->new crosswalk (a 'BNS 103' query must retrieve the IPC-302 line) or filters split across spelling variants. Reliable for marquee statutes via dictionary; long-tail/amendment acts best-effort + cheap-LLM stragglers. The crosswalk is a deterministic lookup the product owns. |
|
| 13 |
+
| `appeal_no / case_number (LIST, with case-type prefix)` | filter/retrieval/dedup/display | moderate | known-item retrieval + dedup + filter UX | SCR/eCourts case-no field + cause-title; regex on type prefix + number + year | Essential for known-item lookup (client gives a diary/appeal number) and linking connected matters. MUST be a LIST — one judgment disposes of many connected numbers. Case-type prefix (Crl.A./C.A./W.P.(C)/SLP) seeds case_type. Verified noisy on legacy corpus ('Appeal (crl.) 197 of 198 197_198 -') — normalize best-effort, display/lookup-grade, don't over-trust as a clean filter. |
|
| 14 |
+
| `bench / coram (list of judges, normalized)` | filter/retrieval/citator/display | moderate | filter UX + known-item retrieval + citator soundness | SCR CORAM/BENCH line; regex split + judge-authority list for canonical IDs | Serves judge-based retrieval and is the raw material for bench_strength. Verified: BENCH/CORAM line parses on 54.5% of legacy corpus (near-100% on SCR). Raw list extraction reliable; canonical judge-ID normalization (honorifics, initials, 'D.Y.' vs 'Dhananjaya Y') is the moderate part and needs a maintained authority list or the facet fragments. |
|
| 15 |
+
| `bench_strength (int + bucket: single/division-2/full-3/constitution-5/larger-7+)` | filter/citator/display | reliable | citator soundness + filter UX + reranking | derived from len(bench); static size->label lookup | DECISIVE LOCK as a CORRECTNESS GATE, not just a 'Constitution Bench only' filter. Free to derive. Read on BOTH ends of every treatment edge: a smaller bench cannot overrule a larger one. FAILS SAFE: where bench is null (verified ~46% of legacy corpus) on either edge end, a treatment='overruled' is DOWNGRADED to doubted/unknown with a provenance flag — never validated as settled law. Reliability inherited from bench parse. |
|
| 16 |
+
| `case_name (raw petitioner + respondent split + canonicalized display/fuzzy string)` | retrieval/display/dedup/filter | reliable | known-item retrieval + filter UX + dedup | SCR cause-title / pet+res structured fields; deterministic normalizer (abbrev/LRs/&Ors stripping) | Most-used human handle and the headline of every results row. Store BOTH raw pet/res AND a canonical fuzzy-match string ('Maneka Gandhi' <-> 'Maneka Gandhi v. Union of India'). Raw extraction reliable; the alias/normalized form is the work. |
|
| 17 |
+
| `cases_cited (edge list of {to_neutral_citation, name, equivalent_citation, treatment, paras/pin-cite})` | citator/retrieval/filter/display | needs-LLM | citator soundness + known-item retrieval | SCR 'Case Law Cited' section for structure (regex); treatment via DeepSeek with conservative 'unknown' default | THE crown jewel and the citator spine — the extraction budget belongs HERE. Two hard rules: (1) every edge resolves to a neutral_citation (else graph unjoinable) — use equivalent_citations as the bridge for pre-2023 cited cases; (2) treatment in {relied_on, referred_to, followed, distinguished, doubted, overruled} defaults to 'unknown' rather than a guessed 'overruled' — get it wrong and you tell a lawyer dead law is live. Names+cites regexable; treatment is needs-LLM. Off-SCR the whole field drops to needs-LLM over body text. Pin-cite hook per edge for grounding. |
|
| 18 |
+
| `court (SC / HC:<state>, specific forum)` | filter/retrieval/citator/display | reliable | filter UX + citator soundness + reranking | connector/provenance (NOT text) — constant 'SC' for Phase 1; eCourts court-code for HC phase | Highest-leverage filter AND an authority guard. Connector-set, not NLP-extracted, hence reliable at scale. MUST encode the specific HC (HC:KA vs HC:DEL), never bare 'HC' — the citator must never let an HC ruling read as overruling an SC one. Model now to avoid Phase-4 migration. |
|
| 19 |
+
| `date (ISO YYYY-MM-DD)` | filter/retrieval/citator/display | reliable | filter UX + known-item retrieval + citator soundness | SCR 'DATE OF JUDGMENT' preamble line; deterministic format normalizer | Powers year/range filters AND the temporal-direction check in the citator (an overruling case must be LATER — auto-rejects impossible edges). Work is normalizing Indian formats (DD-MM-YYYY, '31st January 2000') to ISO; anchor on the labeled line to avoid grabbing reserved/registration date. Validate year against neutral_citation. |
|
| 20 |
+
| `disposition (CLOSED enum: allowed/dismissed/partly_allowed/set_aside/remanded/acquitted/convicted/disposed)` | filter/retrieval/display | moderate | filter UX + reranking + citator soundness | regex over operative ORDER paragraph -> closed vocabulary; coarse reliable, nuance via LLM | REDEFINED from free-text 'outcome'. Verified noise on disk: 'disposed of' (447) and 'disposed' (67) are separate raw values — exactly why a closed enum is mandatory. STRICTLY SEPARATE from good_law_status (an 'allowed' case can now be bad law). Coarse result reliable from stock verbs; partly-allowed / per-party / split-outcome nuance is the LLM part. |
|
| 21 |
+
| `equivalent_citations (LIST of {reporter, volume, page, year})` | filter/retrieval/citator/dedup | moderate | citator soundness + known-item retrieval + dedup | SCR 'Equivalent Citations' table / headnote; reporter regex + LLM fallback for free-text strings | DECISIVE LOCK. The citator's SECOND join key. Verified: good-law gold set is 100% reporter-cite-keyed (SCC/AIR) with 0% INSC; reporter cites appear in 44.2% of legacy corpus vs 0.9% INSC. A single citation STRING is a silent-lookup-failure; must be a LIST so cited-list strings resolve regardless of which reporter was pleaded. Era-dependent nulls (recent INSC-only judgments) are normal coverage. |
|
| 22 |
+
| `good_law_status (DERIVED cached: enum {good_law, overruled, partly_overruled, doubted, per_incuriam, superseded_by_statute, unknown})` | filter/citator/display | needs-LLM | citator soundness + filter UX | computed projection of inbound cases_cited treatment edges + bench_strength validity; recomputed on ingest | DECISIVE LOCK and the product's headline differentiator. DERIVED, not extracted; cached projection refreshed on ingest. MUST DEFAULT TO 'unknown', never 'good_law' — absence of an overruling edge is not proof of soundness. 7-state enum mandatory because the gold set on disk contains a per-incuriam row and an amendment-reversal row a binary model would mis-weight. P0 because it is what the product is sold on; extractability needs-LLM because it depends on the treatment edges feeding it. |
|
| 23 |
+
| `neutral_citation (YYYY INSC N)` | filter/retrieval/citator/dedup | reliable | known-item retrieval + dedup + citator soundness | SCR header/digiSCR; regex /(\d{4})\s+INSC\s+(\d+)/ | Canonical primary key and to-node of every citation edge. Verified present in only 0.9% of legacy 3k corpus — that is a coverage gap (pre-2023 predates INSC), NOT an extraction error. Reliable wherever it exists. NOT the sole join key (see equivalent_citations). |
|
| 24 |
+
| `sections (list of {act, provision, number})` | filter/retrieval/citator | moderate | filter UX + known-item retrieval + citator soundness | SCR headnote/body regex; act-binding via proximity heuristic / cheap LLM | Finest-grained statute filter and a grounding check. Verified ~77% section coverage cited; structured in SCR. Keep {act, number} shape — 's.302' is meaningless without 'IPC'. Bare section list reliable; BINDING the section to the right act when context is ambiguous is the moderate part. Pairs with the BNS crosswalk. |
|
| 25 |
+
|
| 26 |
+
## P1 fields
|
| 27 |
+
|
| 28 |
+
| field | roles | extractability | improves | source | notes |
|
| 29 |
+
|---|---|---|---|---|---|
|
| 30 |
+
| `author_judge` | filter/retrieval/display | moderate | known-item retrieval + filter UX | first all-caps name heading after 'JUDGMENT:' marker; signature block; LLM for multi-opinion attribution | Distinct from coram; matters for weight and judge-search. Moderate because per curiam, 'by the Court', and separate concurring/dissenting opinions complicate 'the' author. Cheap, do not block schema on it. |
|
| 31 |
+
| `case_type / nature (closed taxonomy: civil/criminal/writ/SLP/review/curative/...)` | filter/retrieval/display | moderate | filter UX + reranking | derived from appeal_no prefix + SCR subject; fixed lookup + small classifier | Useful coarse filter ('criminal matters only') and rerank signal. Keep strictly PROCEDURAL (from the case-number prefix); subject-matter/practice-area belongs under keywords, not here. Reliable when appeal_no is clean; small 'other' bucket for composite filings is fine. |
|
| 32 |
+
| `cited_by_count + cites_count (graph degree)` | filter/retrieval/citator/display | reliable | reranking + filter UX | derived from corpus citation graph once cases_cited loaded | Free once cases_cited exists. Strong leading-case/authority proxy and at-a-glance badge (Indian Kanoon's 'Cited by 53834'). Excellent reranking feature. P1 — a signal, not a correctness requirement. |
|
| 33 |
+
| `full_headnote (verbatim)` | retrieval/display/citator | reliable | known-item retrieval + citator soundness | SCR headnote block (raw HTML parse); store pointer per V2 object-store design | Rawest, most reliable artifact and the GROUND TRUTH every derived field is parsed from (provenance + grounding safety net). Bulk text — store the pointer, not in the hot metadata row. Structurally null off-SCR (HC/eCourts have no official headnote); product degrades to body-text chunking there. |
|
| 34 |
+
| `held / ratio (operative holding)` | retrieval/display/citator | moderate | known-item retrieval + reranking + citator soundness | SCR headnote 'Held' VERBATIM where present; LLM grounded against headnote/operative para and FLAGGED as generated where absent | DECISIVE LOCK: its OWN field (not folded into short_summary) but P1 (senior's tier held). The sentence a lawyer quotes as 'what the case decided'. Verified HELD marker in only 3.8% of legacy full texts — off-SCR it is needs-LLM and MUST be grounded against source, clearly flagged generated, or left null. A fabricated holding is the exact liability a grounded citator exists to eliminate. |
|
| 35 |
+
| `issue (issues for consideration)` | retrieval/display | needs-LLM | known-item retrieval + reranking | SCR 'Issues for consideration' header (reframe via LLM into atomic proposition); needs-LLM everywhere off-SCR | DECISIVE LOCK at P1 (paralegal conceded down from P0). Best single field to EMBED for fact-pattern retrieval but an embedding input, not a filter/authority signal — must not block schema. Verified 'issues for consideration' appears in only 0.2% of legacy corpus — needs-LLM everywhere off-SCR. NEVER FABRICATE: where absent, field is null and the row shows held/summary; do not let a thin data region become a hallucination surface in the results row. |
|
| 36 |
+
| `reportable_flag (reportable / non-reportable)` | filter/citator/display | reliable | filter UX + reranking + citator soundness | SCR set-membership provenance (default True) + first-page REPORTABLE stamp regex for HC/eCourts | The corpus's defining authority boundary (Phase 1 IS the reportable set). Near-free from SCR provenance. Verified REPORTABLE stamp in 38.1% of legacy text — OCR/older-convention gaps make text-extraction moderate, but provenance default is reliable for SCR. P1 not P0: a near-constant in Phase 1, a real discriminator (and weaker citator node) only in the HC phase. |
|
| 37 |
+
| `short_summary` | retrieval/display | moderate | filter UX + reranking | SCR case summary, or LLM from issue+held grounded on headnote | 2-3 line gist that makes the 20-row list scannable. Convenience layer — must NOT absorb the structured issue/held fields. Generated version must be grounded against headnote to avoid hallucinated holdings. |
|
| 38 |
+
| `source_url / pdf_path / object_key / scraped_at / content_hash (provenance + dedup)` | display/dedup/citator | reliable | citator soundness (verifiability) + dedup | scraper output (bharat_courts/SCR portal); content_hash = SHA over canonical text; pipeline timestamp | Ship-blocking trust infrastructure: every surfaced proposition must deep-link to the official SCR page/PDF or the answer isn't verifiable. content_hash ADDED — the V2 idempotent/incremental ingest dedups by neutral_citation + hash and catches silently-revised judgments; without an explicit hash the contract is unenforceable. scraped_at drives re-good-law-check staleness. Not filters, but non-negotiable for a grounded citing product. |
|
| 39 |
+
|
| 40 |
+
## P2 fields
|
| 41 |
+
|
| 42 |
+
| field | roles | extractability | improves | source | notes |
|
| 43 |
+
|---|---|---|---|---|---|
|
| 44 |
+
| `advocates / appearances` | display | unreliable | filter UX (niche) | SCR 'Appearance' block where cleanly delimited — display-only, ZERO LLM extraction in Phase 1 | DECISIVE LOCK: DEFERRED. Capture as raw display-only text ONLY where it falls out of a preamble parse for free; build NO LLM extraction pass. Verified ~57% appearance-block coverage but parsing (senior vs AOR vs briefing counsel, multi-line, honorifics) is unreliable via regex. Changes no legal conclusion and competes for the SAME DeepSeek budget as treatment classification — the moat. Field kept forward-compat, never gates retrieval. |
|
| 45 |
+
| `jurisdiction / state` | filter | moderate | filter UX (HC phase only) | derived from appeal_no/court for SC (near-constant); eCourts connector for HC state | DECISIVE LOCK at P2 / forward-compat only. For the national SC, jurisdiction is a near-constant and state is null — surfacing them as live filters now would imply meaning they lack. Define the columns so the HC shard (court x year x state) needs no migration; do NOT render as meaningful Phase-1 filters. |
|
| 46 |
+
| `keywords (court's OWN catchwords only)` | retrieval/filter | moderate | known-item retrieval | SCR 'List of Keywords'/catchwords ONLY — never LLM-generated | DECISIVE LOCK: keep ONLY the court's own catchwords as a cheap BM25/tag-recall layer; LLM-GENERATED keyword lists are CUT — they are drift that competes with the full_headnote embedding. Coverage inconsistent. Demoted to P2. |
|
| 47 |
+
| `lower_court / case_arising_from` | filter/retrieval/display | unreliable | known-item retrieval (appellate chain) | SCR preamble 'arises out of...' narrative; best-effort regex, NO dedicated LLM extraction | DECISIVE LOCK at P2. Genuine appellate-chain use (find the HC judgment under appeal) but lives in free-text prose, so reliable extraction needs LLM — and that budget belongs to treatment classification. Capture where it falls out of the preamble for free; null otherwise. Becomes a real filter in the HC phase. |
|
| 48 |
+
|
| 49 |
+
## Recommended P0 filter set (UI)
|
| 50 |
+
|
| 51 |
+
- court (SC vs HC, and which specific HC — the highest-leverage filter; binding vs persuasive turns on it)
|
| 52 |
+
- year / date range (derived from neutral_citation, free; serves the 'approximate year' vague-recall intent)
|
| 53 |
+
- judge / coram (judge-based retrieval) + bench_strength bucket (Constitution Bench / authority filter)
|
| 54 |
+
- acts + sections (statute-anchored, normalized, carrying the IPC/CrPC/IEA <-> BNS/BNSS/BSA crosswalk)
|
| 55 |
+
- disposition (closed enum: allowed/dismissed/set_aside/... — the 'who won' filter)
|
| 56 |
+
- good_law_status (good/doubted/overruled/... defaulting to 'unknown' — the 'only good law' filter, the product's headline)
|
| 57 |
+
- reportable_flag (the authority axis; near-constant in Phase 1, a real discriminator in the HC phase)
|
| 58 |
+
- case_type (procedural: civil/criminal/writ/SLP) — coarse scoping facet
|
| 59 |
+
|
| 60 |
+
## Resolved debates
|
| 61 |
+
|
| 62 |
+
**Q: Is the clean INSC neutral_citation enough as the citator's join key, or must reporter cites (SCC/SCR/AIR) be a P0 first-class LIST field?**
|
| 63 |
+
→ LOCK equivalent_citations as a P0 LIST of {reporter, volume, page, year} — the citator's SECOND join key; neutral_citation stays the canonical primary key and to-node but is explicitly NOT the sole join key.
|
| 64 |
+
*Verified on disk: INSC appears in only 0.9% of the 3k corpus while the good-law gold set is 100% reporter-cite-keyed with 0% neutral — a neutral-only graph cannot resolve its own ground truth or any pre-2023 landmark, the exact cases the product exists to weigh. A single citation string is a silent-lookup-failure.*
|
| 65 |
+
|
| 66 |
+
**Q: Should good_law_status be a stored filterable field now, and must court-action disposition be split from good-law status?**
|
| 67 |
+
→ LOCK both: a closed disposition enum (court action) kept STRICTLY SEPARATE from a derived, cached good_law_status that DEFAULTS to 'unknown' (never 'good_law') with a 7-state enum {good_law, overruled, partly_overruled, doubted, per_incuriam, superseded_by_statute, unknown}.
|
| 68 |
+
*An 'allowed' case can now be bad law — conflating them tells a lawyer dead law is live. Default 'unknown' because absence of an overruling edge is not proof of soundness. 7 states because the gold set on disk contains per-incuriam and amendment-reversal rows a binary model mis-weights. On-disk 'disposed'/'disposed of' duplication proves the free-text outcome must become a closed enum.*
|
| 69 |
+
|
| 70 |
+
**Q: Is bench_strength a P0 correctness requirement or a cheap nice-to-have filter, and does the validity gate fail open or closed?**
|
| 71 |
+
→ LOCK P0 as a correctness GATE read on BOTH edge ends; it FAILS SAFE — a missing/smaller bench_strength on either end downgrades a treatment='overruled' to doubted/unknown with a provenance flag, never validating it.
|
| 72 |
+
*A smaller bench cannot overrule a larger one; without strength on both ends the citator emits legally void 'overrulings' as settled law. Free to derive (len(bench)). Verified bench parses on only 54.5% of legacy corpus — the gate must hold back, never wave through, an unverifiable small-bench overruling of a Constitution Bench.*
|
| 73 |
+
|
| 74 |
+
**Q: Must HELD be a separately extracted field, and is it P0 or P1?**
|
| 75 |
+
→ LOCK HELD as its OWN field (not folded into short_summary) at P1; verbatim from the court's headnote where present, LLM-generated only when grounded against source text and flagged, null where it cannot be grounded.
|
| 76 |
+
*HELD is the sentence quoted as 'what the case decided' — burying it in a generic summary makes it unquotable. P1 (not P0) because it is a retrieval/display input, not a filter/authority signal, and must not block the schema. Verified HELD marker in only 3.8% of legacy texts — off-SCR it is needs-LLM; a fabricated holding is the precise liability a grounded citator exists to eliminate.*
|
| 77 |
+
|
| 78 |
+
**Q: Is 'issue' a reliable P0 extract or a needs-LLM P1 field?**
|
| 79 |
+
→ LOCK P1, extractability needs-LLM, embed-where-present, NEVER fabricate — null off-SCR with the row falling back to held/summary.
|
| 80 |
+
*Best field to embed for fact-pattern retrieval but an embedding input, not a filter. Verified 'issues for consideration' appears in only 0.2% of legacy corpus — treating it as reliable P0 display would fill the results row (the surface a lawyer trusts at a glance) with empty or hallucinated issues exactly where data is thinnest.*
|
| 81 |
+
|
| 82 |
+
**Q: Collect advocates in Phase 1, or cut as vanity?**
|
| 83 |
+
→ LOCK DEFER: P2, display-only, captured ONLY where it falls out of a preamble parse for free, with ZERO dedicated LLM extraction in Phase 1.
|
| 84 |
+
*Advocate names change no legal conclusion and 'cases argued by X' is a niche BD query, not a precedent-finding intent. Verified ~57% block coverage but parsing is unreliable via regex. It competes for the SAME DeepSeek budget as cases_cited treatment — the moat and highest-liability field. Spend the budget on treatment, not advocate-name parsing.*
|
| 85 |
+
|
| 86 |
+
**Q: Keep keywords at all, and is the normalized topic_path taxonomy in Phase-1 scope?**
|
| 87 |
+
→ LOCK keywords at P2 — the court's OWN catchwords ONLY, never LLM-generated. DEFER the topic_path taxonomy out of Phase 1.
|
| 88 |
+
*An LLM-generated keyword list is drift that competes with the full_headnote embedding. A hand-maintained hierarchical taxonomy is real ongoing cost for a 'browse by area of law' intent that semantic search over the headnote already serves passably; build it only when browse demand is demonstrated.*
|
| 89 |
+
|
| 90 |
+
**Q: Keep lower_court / jurisdiction / state in Phase 1, or defer to the HC phase?**
|
| 91 |
+
→ LOCK differentiated: lower_court P2 (free preamble parse only, no LLM); jurisdiction + state P2 / forward-compat columns only, NOT rendered as meaningful Phase-1 filters.
|
| 92 |
+
*lower_court's appellate-chain use is genuine but lives in free-text needing LLM budget owed to treatment. For the national SC, jurisdiction is near-constant and state is null — surfacing them as live filters implies meaning they lack until HC sharding (court x year x state) in Phase 4. Define the columns to avoid migration; extract nothing on spec.*
|
| 93 |
+
|
| 94 |
+
**Q: Should appeal_no / case_number be P0 (paralegal/feasibility) or P1 (senior)?**
|
| 95 |
+
→ LOCK P0, stored as a LIST, with the case-type prefix retained as the source of case_type.
|
| 96 |
+
*Essential for known-item lookup before a neutral citation exists and for linking connected matters; one judgment disposes of many numbers so a scalar loses lookups. Verified noisy on the legacy corpus — normalize best-effort, treat as lookup/display-grade not a clean filter.*
|
| 97 |
+
|
| 98 |
+
**Q: Is content_hash needed given it was only implied by the V2 design?**
|
| 99 |
+
→ LOCK ADD content_hash (SHA over canonical text) as a P1 dedup field alongside source_url/pdf_path/scraped_at.
|
| 100 |
+
*The V2 ingestion DAG dedups by neutral_citation + content hash and only re-embeds changed docs; without an explicit hash field the idempotent/incremental contract is unenforceable, and it catches silently-revised judgments.*
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
## Open questions (need a human / lawyer call before the citator writes states)
|
| 104 |
+
|
| 105 |
+
- good_law_status edge-case modeling: the gold set contains a 'per incuriam' row and a 'reversed mainly by constitutional amendment, not a clean SC overrule' row. The 7-state enum names these, but the DERIVATION logic (what edge pattern + bench-strength condition yields per_incuriam vs superseded_by_statute vs partly_overruled) is unspecified and needs a human-authored rule set with a lawyer in the loop before the citator writes these states.
|
| 106 |
+
- Citator join across the era boundary: cases_cited edges from pre-2023 judgments reference cited cases by reporter cite only. Resolving those to a neutral_citation to-node depends on a complete equivalent_citations index — needs a decision on what happens when a cited case has NO neutral citation at all (legacy-only landmark): does the edge resolve to a reporter-cite-keyed node, and is that node a first-class citator vertex?
|
| 107 |
+
- Fail-safe-closed gate calibration: downgrading every missing-coram overruling to doubted/unknown is safe but will suppress real overrulings on ~46% of the legacy corpus where bench doesn't parse. Worth a human decision on whether to prioritize a bench-line backfill pass (re-parse / LLM) for high-inbound-degree cases so landmark overrulings aren't silently withheld.
|
| 108 |
+
- HELD/issue off-SCR fallback UX: the rule is null-never-fabricate, but the product still needs a defined results-row fallback order (held -> short_summary -> first headnote sentence -> ??) for the majority HC/eCourts corpus where none of issue/held/headnote exist. Needs a UX + grounding decision.
|
| 109 |
+
- Whether disposition's per-party and split-outcome nuance ('allowed for appellant A, dismissed for B; remanded on one issue') is in Phase-1 scope or deferred — the closed enum captures the headline result but multi-appeal judgments carry split outcomes the single enum value cannot represent.
|
| 110 |
+
- Treatment-classification confidence threshold and human review: cases_cited treatment is the highest-liability field. Needs a decision on the DeepSeek confidence cutoff below which an edge stays 'unknown', and whether overruled/doubted classifications get a mandatory human-review queue before they flip a downstream good_law_status.
|
phase1/SESSION1_CHANGE_PLAN.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# THEMIS — DECISION-GRADE CHANGE PLAN
|
| 2 |
+
(Panel chair synthesis. All 15 expected cases are in corpus; every failure below is pipeline, not coverage.)
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
## 1. WHAT THE 11 QUERIES PROVE
|
| 7 |
+
|
| 8 |
+
**Root cause A — Query-side vocabulary gap → pool-recall failure (dominant: 5/11 queries).**
|
| 9 |
+
The lawyer writes fact-narratives ("summons never served", "document discovered after evidence closed"); the controlling judgments speak statute (O9R13, O41R27, s.14 Limitation, s.43 TPA). BGE-small + BM25 cannot bridge this, so the gold case never enters the pool: **Q4** (Nortel), **Q5** (Ibrahim Uddin), **Q7** (Parimal), **Q3** (Avitel), **Q11** (Lala Durga Prasad). Two sub-forms:
|
| 10 |
+
- *Enumerable procedural postures* (Q4/Q5/Q6/Q7): a fixed, small set of posture→controlling-authority mappings every junior lawyer knows. Q6 is the ranking variant of the same gap (Revajeetu in pool but 4th).
|
| 11 |
+
- *Doctrinal issue statements* (Q9 s.43 ostensible-owner, Q11 s.44 co-owner alienation): the issue phrase appears nowhere in the query; raw-text vectors match scenery, not ratio.
|
| 12 |
+
Corollary: **the Q4/Q5 abstentions were honest-given-pool but fired on a repairable pool** — abstain is downstream of retrieval with no repair loop. Plus one deterministic recall hole: **Q5's "Ibrahimuddin" ≠ "Ibrahim Uddin"** name-index bug (no LLM involved).
|
| 13 |
+
|
| 14 |
+
**Root cause B — Authority signal starvation (3-4/11 queries, ranking mode).**
|
| 15 |
+
63k edges over 37,898 judgments ≈ 1.7 edges/judgment vs a true ~10-20 cites/judgment (~500k-1M edges): <15% of the graph extracted. The lawyer's Q1 rubric — "most COMMONLY USED first, weighted by how many later orders relied on it" — is literally usage-weighted in-degree with treatment labels, and it is currently **uncomputable**. This is why Revajeetu sits 4th (**Q6**), Thomson Press falls to the extended tier (**Q10**), Q1/Q8 ordering disappoints, and cited-by expansion from Ayyasamy/World Sport cannot pull in Avitel (**Q3**). It also starves the ranker of deterministic tiebreaks, feeding root cause C.
|
| 16 |
+
|
| 17 |
+
**Root cause C — Lawyer-visible nondeterminism (Q2: rank 1 → rank 4 on identical re-run).**
|
| 18 |
+
Final order currently emerges from an LLM judge plus tie-breaking on noisy rerank scores. Where the variance enters is **unverified** — the panel's bet is frame() naming different landmarks per run, not the judge — so it must be localized before it's fixed. The litigator's line is the product truth: rank-4-instead-of-1 is forgivable; a different answer to the same question is a slot machine, and trust never recovers.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## 2. THE CHANGES (value-per-effort order; ⟂ = runs in parallel)
|
| 23 |
+
|
| 24 |
+
**C0 — Instrumentation gate + frozen lawyer-gold slice.** *Days 1-2. Nothing else ships before this exists.*
|
| 25 |
+
- Freeze the 11 queries + 15 expected authorities as named must-pass assertions (not an aggregate — n=11 has a 0.28-0.79 CI; one flip = 9 points).
|
| 26 |
+
- Log per stage: frame() output, per-lane pools, **PoolRecall-pre-judge per expected case**, judge order. Metrics: Expected@3, nDCG@5 against the lawyer's stated Q1 ordering, Stability@5 (5× runs, ~$0.50, ~20 min; report top-3 Jaccard + max rank displacement), abstain-rate conditional on PoolRecall.
|
| 27 |
+
- Run nightly. Baseline today: ~9/15 expected cases reach the pool. Guard the 78q benchmark with paired per-query McNemar over 3 averaged runs (±2-3pt variance swamps small deltas otherwise).
|
| 28 |
+
- *Fixes:* nothing directly; prevents misattributing every other fix. *Cost:* 1-2 eng-days, ~$0.50/run.
|
| 29 |
+
|
| 30 |
+
**C1 ⟂ — Name-variant normalizer.** *Half a day.*
|
| 31 |
+
- Known-item index: whitespace/punct-collapse, honorific stripping (Shri/Smt/M/s), -uddin/-ud-din collapse, Mohammad/Md variants, v./vs./versus; edit-distance ≤2 fallback. Alias table later enriched by C4's observed citation strings.
|
| 32 |
+
- *Fixes:* Q5 lookup bug. *Measured:* variant unit tests (both spellings exact-resolve), zero regression risk. Both spellings added to the frozen slice.
|
| 33 |
+
|
| 34 |
+
**C2 — Posture→controlling-authority spine + lookup-before-abstain.** *Week 1; ~2-3 hrs founder + 2-3 eng-days.*
|
| 35 |
+
- ~30 YAML rows, founder-authored: {posture phrase, provision, controlling docid, 1-line ratio}. Rows enter by PR; CI acceptance = docid exact-resolves AND one deep-read returns verdict=controls on a canonical posture query. Every future session failure adds a row.
|
| 36 |
+
- frame() gains a `procedural_posture` enum field (same single DeepSeek call — zero marginal cost/latency). Spine hit injects the mapped case as a **protected candidate that must still earn rank via deep-read verdict=controls** — never a hard slot-1 pin (a misclassified posture must not override the grounding gate).
|
| 37 |
+
- **Reorder the abstain path:** weak-pool signal (all reads background/irrelevant OR rerank margin < threshold) → posture lookup + one hint re-retrieval + deep-read of injected case → only then abstain. Repair path costs +$0.01-0.02, +5-10s, abstain-path only.
|
| 38 |
+
- *Fixes:* Q4, Q5, Q7 (recall), Q6 (ranking). *Measured:* PoolRecall 4/4 on Q4/5/6/7; Expected@3 ≥9/11; predicted hit@1 0.71→~0.78. Additive, 78q-safe.
|
| 39 |
+
|
| 40 |
+
**C3 — Determinism package.** *Week 1, overlapping C2. Details in §4.*
|
| 41 |
+
- *Fixes:* Q2. *Measured:* Stability@5 max displacement ≤1. **No ranking change lands before this passes** — you cannot attribute ranking gains while run swings exceed effect sizes.
|
| 42 |
+
|
| 43 |
+
**C4 ⟂ — Single full-corpus batch pass (launch the overnight job NOW; integration gated).** *~$125, 6-10 hrs at 60-80 concurrent; zero query-time cost.*
|
| 44 |
+
- Step 1 (free, same day): regex skeleton over SCC/AIR/SCR patterns — validates the density hypothesis and serves as a cross-check, **not** the extraction (Indian SC judgments cite half their authorities by bare name; regex gives no treatment labels, no aliases).
|
| 45 |
+
- Step 2: one DeepSeek read of all 37,898 full texts, **three outputs per judgment**: (a) every cited case, raw string + resolved docid + treatment label {followed/relied/distinguished/overruled/referred}; (b) raw citation strings as observed aliases → feeds C1's table systematically; (c) posture + issue tags (O6R17, O9R13, O41R27, s.14 Limitation, s.43/s.44 TPA…) + 1-line holding.
|
| 46 |
+
- **Integration gates before anything touches lawyer-visible ranking:** in-edge sanity on 20 known landmarks; treatment-label precision ≥0.9 on a 100-edge hand audit.
|
| 47 |
+
- *Fixes:* enabler for Q1/Q8/Q10 ordering, Q3-Avitel via cited-by expansion, Q9/Q11 via issue tags, Q5-class aliases. *Measured:* standalone sanity metrics first, then C5's gates.
|
| 48 |
+
|
| 49 |
+
**C5 — Authority prior + the ranking objective (§3) + cited-by expansion in the doctrine lane.** *Week 2, after C3 passes and C4 audits clear.*
|
| 50 |
+
- *Fixes:* Q1, Q6, Q10 ordering; Q3 Avitel recall. *Measured:* 11-slice must-passes + McNemar'd 78q over 3 runs.
|
| 51 |
+
|
| 52 |
+
**C6 ⟂ — "Deeper explanation" card for the top pick.** *~1 day.*
|
| 53 |
+
- Surface what deep-read already produced: the test laid down, paragraph pinpoint, one line on later-bench application, why it controls THIS posture. Plus a "cite this in court for X" tag per pick.
|
| 54 |
+
- *Fixes:* Q3 feedback ("no deeper explanation"). *Measured:* qualitative; founder review.
|
| 55 |
+
|
| 56 |
+
**C7 — CONDITIONAL: Workstream B issue-vector arm (~$200).** *Only if Q9/Q11 still fail after C4's issue tags are embedded as a retrieval arm and C5's graph expansion lands.* The batch pass already extracts issue/holding text — embed that first; don't pay for a second full read.
|
| 57 |
+
|
| 58 |
+
**Expected end state:** session slice ~5-6/11 clean → ~9-10/11; hit@1 ~0.71 → ~0.78-0.80; zero lawyer-visible rank flips.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## 3. THE RANKING OBJECTIVE (the lawyer's rubric, made implementable)
|
| 63 |
+
|
| 64 |
+
The Q1 rubric = **usage-weighted, coverage-diverse, minimal ordering with courtroom-use tags**. It lives in a deterministic assembly step AFTER the judge — never in the judge prompt (with a 63k-edge graph the LLM cannot estimate "most used" and will hallucinate it).
|
| 65 |
+
|
| 66 |
+
**Division of labor:** the judge (and deep-reads) emit *verdicts and facet annotations only*. Final order is a pure function of the pool:
|
| 67 |
+
|
| 68 |
+
```
|
| 69 |
+
sort key (descending priority):
|
| 70 |
+
1. verdict tier controls > supports > background (from deep-read)
|
| 71 |
+
2. spine/seminal flag lawyer-authored spine hit (from frame() lookup)
|
| 72 |
+
3. authority prior Phase 1: raw recency-weighted in-degree (post 20-landmark sanity)
|
| 73 |
+
Phase 2: treatment-weighted in-degree per doctrine cluster,
|
| 74 |
+
relied/followed only (post ≥0.9 label audit)
|
| 75 |
+
4. year (desc), then docid — guarantees a total order, no residual ties
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
**Slot 2 = maximum marginal coverage:** MMR over ratio embeddings among controls/supports — the heavily-used case answering the facet slot 1 does not (Wakf-Board-after-Lakshmi-Reddy behavior, deterministic).
|
| 79 |
+
**Stop rule:** cut at 3-4 picks once frame()'s issue facets are covered; do not pad to 6. Extended tier absorbs the rest.
|
| 80 |
+
**Per-pick tag:** "cite this in court for X" — ratio + pinpoint from the deep-read verdict card.
|
| 81 |
+
**Explicitly deferred:** "usage-for-THIS-proposition" attribution (litigator's key 1 in full). It stacks unvalidated proposition-alignment on unaudited treatment labels. Raw in-degree ships first; treatment weighting graduates after the 100-edge audit; proposition-level attribution only if a measured gap remains.
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
## 4. STABILITY FIX (Q2 must never happen again)
|
| 86 |
+
|
| 87 |
+
Ordering must be a **pure function of the query** — LLM stays where it is invisible and cacheable; determinism owns everything the lawyer sees.
|
| 88 |
+
|
| 89 |
+
1. **Localize first (one afternoon, ~$0.50):** run the 11 queries 5×, diff frame() output / lane pools / judge order per run. Panel's bet: frame() names different landmarks run-to-run, changing the pool itself — if true, judge-side fixes alone change nothing.
|
| 90 |
+
2. **Upstream:** frame() at temperature 0 AND cached per normalized query (post-C1 normalization). Same query ⇒ byte-identical frame ⇒ identical pool.
|
| 91 |
+
3. **Downstream:** judge emits verdicts/slots only; final rank from the §3 sort key, terminating in docid — total order by construction, ties impossible.
|
| 92 |
+
4. **Middle:** cache deep-read verdicts per (query-hash, doc-id).
|
| 93 |
+
5. **Verify, don't assume:** Stability@5 nightly on the frozen slice; targets: max displacement of expected cases ≤1 (Q2's swing was 3), top-3 Jaccard ≈ 1.0.
|
| 94 |
+
6. **Only if residual verdict flips still break Stability@5:** add majority-of-3 on verdict disagreement (parallel, ~+$0.01/q). Not before — three panelists independently flagged it as premature spend on an unlocalized hypothesis.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## 5. WHAT NOT TO DO
|
| 99 |
+
|
| 100 |
+
- **No majority-of-3 judging now** — deterministic sort removes ordering variance; voting is a contingency, not a default.
|
| 101 |
+
- **No hard slot-1 spine pinning** — spine rows are protected candidates that must earn verdict=controls; the grounding gate stays the arbiter. A misclassified posture must degrade gracefully, not inject a wrong "controlling" case.
|
| 102 |
+
- **No regex-only citation graph as final extraction** — misses name-only cites ("in *Ayyasamy*"), carries no treatment labels; ranking by raw untreated in-degree promotes heavily-*distinguished* cases, and citing a distinguished authority as controlling loses the lawyer credibility in court. Regex = skeleton + sanity check only.
|
| 103 |
+
- **No graph feature in ranking before audits pass** (20-landmark in-edge sanity; treatment precision ≥0.9 on 100 hand-labeled edges).
|
| 104 |
+
- **No "most used" estimation inside the judge prompt** — the LLM will confabulate usage the graph can't support.
|
| 105 |
+
- **No proposition-level usage ranking yet** (see §3).
|
| 106 |
+
- **Don't fund Workstream B's separate $200 summary pass yet** — C4's issue tags ride the same read for free; check Q9/Q11 against them first. Never pay for the same full-corpus read twice.
|
| 107 |
+
- **No bigger index, no new embedding model, no re-embedding** — coverage is proven not to be the problem; every dollar there is misdirected.
|
| 108 |
+
- **Don't treat n=11 as a statistical gate** — named must-pass assertions only; never edit frozen golds (freeze weekly snapshots as sessions accrue); don't trust the n=7 statute slice; McNemar-guard the 78q.
|
| 109 |
+
- **Don't book unmeasured wins** — "+10-15 recall@20 for summary arms" is a literature prior, not a Themis measurement.
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## 6. THE ONE FOUNDER DECISION
|
| 114 |
+
|
| 115 |
+
**Commit Themis to a founder-curated editorial layer: the posture→controlling-authority spine, with you as its named editor.**
|
| 116 |
+
|
| 117 |
+
Everything else in this plan is engineering the panel already converged on ($125 batch job, determinism, normalizer — none founder-level). The spine is different in kind: each row is Themis asserting, on your professional authority, "*this* is THE controlling case for *this* posture." That makes Themis a hybrid curated-plus-retrieval product, not a pure algorithm — with your ongoing obligation attached (~2-3 hours now for 30 rows, ~15 min/week as session failures add rows via PR, and ownership of any row that's wrong in front of a lawyer).
|
| 118 |
+
|
| 119 |
+
Decide yes/no on that commitment. If yes, it converts 4 of 11 session failures this week at near-zero cost, and the curated spine + treatment-labeled graph together become exactly the citator-style moat the competitive analysis said Themis needs. If no, Q4/Q5/Q6/Q7-class queries wait on the slower, less certain representation track.
|
| 120 |
+
|
| 121 |
+
The panel's recommendation is **yes** — with the safeguard already built in: spine entries are protected candidates gated by deep-read verdicts, so your editorial judgment is always checked against the text of the judgment itself before a lawyer sees it.
|
phase1/TWO_LAYER_PLAN.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# THEMIS — DECISION PLAN: THE TWO-LAYER ITERATION
|
| 2 |
+
|
| 3 |
+
## 1. THE CORE INSIGHT
|
| 4 |
+
|
| 5 |
+
The lawyer's observation diagnoses where the pipeline is blind, not what it lacks: retrieval already finds the right cases (hit@any 0.85), but every decision that matters — ranking, relevance verdicts, abstention — is made by a judge reading 700 characters. Moving comprehension to full judgment text at decision time (Workstream A) attacks the 0.85→0.55 gap, the query-4 false-authority failure, and quote quality simultaneously. But deep-read's ceiling is exactly hit@any: it cannot rank what retrieval never surfaced, so Workstream B separately owns the 15% recall gap (query-3 class). Fund both; never expect one to do the other's job.
|
| 6 |
+
|
| 7 |
+
## 2. WORKSTREAM A — THE DEEP-READ AGENTIC LOOP
|
| 8 |
+
|
| 9 |
+
**Trigger policy.** Shallow path stays default. Go deep when: doctrine/statute lanes dominate the candidate pool, cross-encoder top-1 margin is flat, or judge confidence < threshold. Never deep-read when the known-item lane fires (it's at 1.00). "~40% of queries go deep" is a hypothesis to measure, not a design input.
|
| 10 |
+
|
| 11 |
+
**Which docs.** Top-8 post-cross-encoder candidates, union across lanes, dedup by case.
|
| 12 |
+
|
| 13 |
+
**What is read (chair ruling on tiered-vs-full).** Full judgment text up to a 25k-token cap — this covers the large majority at avg ~17k tokens. Legal-researcher is right that in multi-issue SC judgments the controlling passage sits mid-judgment, exactly where head/tail packs go blind, and the cost delta ($0.013 vs $0.037) is noise. Latency-systems is right that *output* tokens are the latency tail — so read full, extract lean. Only above-cap monsters (Kesavananda-class) get the tiered pack: HELD + first 2k + last 3k + ±1.5k around every hit chunk, with escalation hard-capped at 2 concurrent, picked by reranker score.
|
| 14 |
+
|
| 15 |
+
**Extraction (one DeepSeek call per doc, all 8 parallel, strict JSON, prompt ordered `[judgment][query]` for prefix caching — bake this in now, retrofitting invalidates the cache):**
|
| 16 |
+
```
|
| 17 |
+
{verdict: controls|supports|background|irrelevant, confidence,
|
| 18 |
+
ratio_one_liner, exact_passage (verbatim), what_it_does_NOT_decide,
|
| 19 |
+
missing_doctrine_hint}
|
| 20 |
+
```
|
| 21 |
+
No treatment extraction (followed/distinguished/overruled) in the query path — that's an offline batch job.
|
| 22 |
+
|
| 23 |
+
**Feeds judge + grounding.** judge() ranks from cards, not chunks. `exact_passage` passes through the existing substring gate unchanged — the one hard rule survives, and pass rates should rise because the reader saw real text.
|
| 24 |
+
|
| 25 |
+
**Cost/latency.** Worst case ~136k input ≈ $0.037/query; wall clock ≈ slowest parallel call. Deep mode lands ~30–35s total; SLO is p95 ≤ 60s, not the mean. SSE progress mandatory: candidates at ~5s, each read as it completes, answer last.
|
| 26 |
+
|
| 27 |
+
**Honesty mechanism (query 4).** Three-tier output: **controlling** / **persuasive-only** (weak authority, flagged as weak) / **no SC authority** — the last emits a fixed template: "No strong Supreme Court authority found — this area has developed principally in High Courts." Initial bar: abstain when no card reaches supports/confidence ≥ 0.6 — but the threshold is *swept* on the 15 no-answer benchmark queries and reported as a FAR-vs-hit@3 frontier; pick the operating point from the curve, never tune one side alone. `what_it_does_NOT_decide` is what makes "the bail case is irrelevant" detectable.
|
| 28 |
+
|
| 29 |
+
**Query-3 fix (missed controlling doctrine).** Primary, deterministic: add a relief-sought/procedural-posture field to frame() plus a ~30-row remedy→controlling-authority spine curated by Hitin (posture = quashing → Bhajan Lal / Pepsi Foods / Neeharika enter the doctrine lane as known-items). Validated on *held-out* posture queries, not the 4 we've seen. Fallback for uncovered postures: exactly ONE bounded `missing_doctrine_hint` re-retrieval round, then stop.
|
| 30 |
+
|
| 31 |
+
## 3. WORKSTREAM B — REPRESENTATION UPGRADE
|
| 32 |
+
|
| 33 |
+
**What to embed.** Chunks stay (grounding gate needs them; known-item/fact already 1.00/0.82). Add ~3 doc-level vectors per judgment — issues / holding / facts — from DeepSeek-generated structured JSON, anchored on HELD where present (56% checkable). ~114k new vectors beside 1.3M; minutes to embed on the 5060 Ti. Field-routed from frame(): `doctrine_issue`→issues vector, `fact_query`→facts vector, added as an RRF arm, never a replacement. **Summaries are retrieval keys only, never evidence** — judge and grounding operate on original text, so a hallucinated summary can only inject a candidate that downstream filters kill. Share one schema between deep-read cards and summary JSON so query-time reads back-fill the index. ISSUE field (3%) is dead; ignore it.
|
| 34 |
+
|
| 35 |
+
**Generation plan + cost.** Full corpus ≈ 430–650M input tokens ≈ $175–200 (off-peak batch discount can roughly halve it); monsters map-reduced at a 60k cap.
|
| 36 |
+
|
| 37 |
+
**Ablation-first protocol.** Stage 0 (today, $0): HELD-vector RRF arm in the doctrine lane; run the frozen rig. Stage 1 (~$27): 5k-judgment pilot — all gold docs for the rig subset + random distractors — one variable at a time: baseline (0.517) vs +HELD vs +summary vs both. Stage 2: corpus-wide spend only on pilot pass.
|
| 38 |
+
|
| 39 |
+
**Expected lift / kill criteria.** Hypothesis: +3–6 nDCG on doctrine queries, hit@1 0.55→~0.62. Kill if: <+0.02 nDCG@10 on the 800q rig, OR any lane regresses (known-item must hold 1.00), OR hit@3 not non-inferior on the 128q set. No re-embedding of 1.3M chunks and no embedder swap until added-lane gains plateau.
|
| 40 |
+
|
| 41 |
+
## 4. WHAT NOT TO DO
|
| 42 |
+
|
| 43 |
+
- **Per-query treatment extraction** (followed/distinguished/overruled cards) — a corpus-annotation project smuggled into the query path; +5–8s of generation per call. Do it offline later; it repairs the citation graph as a byproduct.
|
| 44 |
+
- **The (case_id, issue_hash) extraction cache and three-layer cache plumbing** — pilot queries are near-unique; hit rate ~0. Keep only DeepSeek prefix caching. Revisit at real traffic.
|
| 45 |
+
- **The 50-entry HC-dominant-doctrine list** before measuring whether reader-abstention alone hits FAR ≤5% — curated lists are maintenance debt and tuning-on-the-test-set; buy them only if the model can't earn the gate, and evaluate on held-out queries.
|
| 46 |
+
- **Corpus-wide summary spend before the $27 pilot gate.**
|
| 47 |
+
- **Open-ended agent loops** — one bounded re-retrieval round, then stop.
|
| 48 |
+
- **PageRank on the 63k-edge graph** (already rejected; the graph is under-extracted, not under-ranked).
|
| 49 |
+
- **Deep-reading the known-item lane** (it's at 1.00; only downside risk).
|
| 50 |
+
|
| 51 |
+
## 5. BUILD ORDER
|
| 52 |
+
|
| 53 |
+
1. **Freeze the 128q benchmark + FAR metric** (week 1; Hitin authors 25 real queries, 15 no-answer, 10 controlling-doctrine; 78q absorbed). Blocking for every ship gate below. *Runs parallel with step 2.*
|
| 54 |
+
2. **Free wins, $0** (week 1, *parallel with 1*): widen judge window to HELD + ~3k chars per candidate (+$0.004/query, +2s); add the HELD-vector RRF arm to the doctrine lane. Validate on the frozen 800q rig; re-baseline on 128q once frozen. This is experiment zero — if hit@1 moves to ~0.60 on context alone, the deep-read mechanism is confirmed before any architecture is built.
|
| 55 |
+
3. **Posture field + remedy→authority spine** (week 2, *parallel with 4*): Hitin curates ~30 rows over a weekend; gate on held-out posture queries.
|
| 56 |
+
4. **Deep-read mode** (weeks 2–3): three-arm A/B on all 128q for <$10 — 700-char vs widened vs deep-read. Ship gates: hit@1 ≥ +0.05 over the *widened* arm, pick-change rate ≥15%, p95 ≤60s, plus 40 blind lawyer-judged why-line pairs (never DeepSeek grading DeepSeek).
|
| 57 |
+
5. **Abstention operating point** (week 3, rides on 4): sweep the threshold, publish the FAR-vs-hit@3 frontier, ship the three-tier template. Gate: FAR ≤5% AND hit@3 non-inferior on the same frozen run.
|
| 58 |
+
6. **Summary pilot → corpus** (weeks 3–4, *parallel with 4–5*): $27 5k-judgment pilot; ~$200 corpus-wide off-peak run only on ≥+0.02 nDCG@10 with known-item = 1.00 guard.
|
| 59 |
+
|
| 60 |
+
## 6. THE ONE FOUNDER DECISION
|
| 61 |
+
|
| 62 |
+
**Adopt FAR ≤5% (jointly with hit@3 non-inferiority) as the pilot go/no-go gate — committing Themis to answering "no strong SC authority, likely a High Court matter" rather than ever confidently citing a weak case, even at the cost of a few points of answer rate.** This single call fixes the abstention operating point, defines pilot success, and commits Hitin's ~3 days of scarce time (benchmark authoring, spine curation, blind grading) that the entire measurement discipline depends on. Everything else in this plan is reversible engineering; this is the product's identity — a lawyer forgives a miss, never a confident wrong citation.
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## AMENDMENT (post-Hitin call): the eval-set reality
|
| 66 |
+
|
| 67 |
+
Hitin's input: **building an eval set is very hard because even lawyers don't know the not-so-famous
|
| 68 |
+
precedents** — settled law and famous precedents anyone can find; the tail is beyond recall. This is
|
| 69 |
+
simultaneously the product's value proposition and the reason gold labels can't be authored directly.
|
| 70 |
+
|
| 71 |
+
**Recalibrated gold strategy — recall is hard, verification is easy:**
|
| 72 |
+
1. **Settled/famous** — Hitin labels directly (fast; also the 30-row posture→authority spine is
|
| 73 |
+
settled doctrine, squarely inside what lawyers know cold).
|
| 74 |
+
2. **Long tail — gold by construction:** reverse-generate queries FROM known cases (the silver-set /
|
| 75 |
+
78q methodology): the source case IS the answer, no recall required. Primary tail-gold source;
|
| 76 |
+
generate harder variants (multi-issue, posture-heavy) the same way.
|
| 77 |
+
3. **Real queries — pooled verification (TREC-style):** collect real lawyer queries (cheap), run
|
| 78 |
+
Themis (+ competitor side-by-side), Hitin BLIND-GRADES pooled results relevant/not — each session
|
| 79 |
+
incrementally builds qrels. Relative judgment (A vs B) is easier still.
|
| 80 |
+
|
| 81 |
+
**Build-order change:** Step 1 becomes "Hitin supplies queries + blind-verifies pooled results"
|
| 82 |
+
(not "authors gold answers"). His scarce hours go to verification, the only method that scales to
|
| 83 |
+
the tail. The FAR/no-answer set is unaffected (knowing "this is HC territory" is what lawyers DO know).
|
phase1/deploy/Caddyfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
themis.apexflo.ai {
|
| 2 |
+
# Auto Let's Encrypt TLS for the subdomain (needs the DNS A record + ports 80/443 open).
|
| 3 |
+
# uvicorn listens only on 127.0.0.1:8000; Caddy is the public edge.
|
| 4 |
+
reverse_proxy 127.0.0.1:8000 {
|
| 5 |
+
flush_interval -1 # stream SSE (search_stream / deep_search_stream) without buffering
|
| 6 |
+
}
|
| 7 |
+
encode gzip
|
| 8 |
+
request_body {
|
| 9 |
+
max_size 5MB
|
| 10 |
+
}
|
| 11 |
+
}
|
phase1/deploy/DEPLOY.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Themis cloud deploy runbook (Hetzner CPX41 + Caddy + themis.apexflo.ai)
|
| 2 |
+
|
| 3 |
+
Serving is **CPU-only** — no GPU. The box just loads the prebuilt artifacts and serves.
|
| 4 |
+
|
| 5 |
+
## Prereqs (George)
|
| 6 |
+
- Hetzner CPX41 (8 vCPU / 16 GB / 240 GB), Ubuntu 24.04.
|
| 7 |
+
- My key in `~/.ssh/authorized_keys`: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKHKQSaIlbdEE3b+46UWMn6E2+8kplUvOuseq7+ikQYx claude-thor-access`
|
| 8 |
+
- DNS: `themis.apexflo.ai` A record → box IP.
|
| 9 |
+
- Hetzner cloud firewall: allow inbound 22, 80, 443. (8000 stays internal.)
|
| 10 |
+
|
| 11 |
+
## What gets uploaded from the Mac (5.8 GB)
|
| 12 |
+
From `phase1/data/thor_artifacts/`: escr_vectors.npy, escr_chunks.jsonl, escr_corpus_full.jsonl, escr_meta.jsonl, edges.jsonl, good_law.jsonl, escr_pdfmap.jsonl
|
| 13 |
+
From `phase1/scripts/`: serve.py, frontend.html
|
| 14 |
+
Secret: `themis/.env` with `DEEPSEEK_API_KEY`, `CLERK_SECRET_KEY`, and the
|
| 15 |
+
Clerk public/configuration values documented in
|
| 16 |
+
[`documentation/15_CLERK_AUTHENTICATION.md`](../../documentation/15_CLERK_AUTHENTICATION.md).
|
| 17 |
+
Use mode 600. Never commit it or paste the secret key into chat.
|
| 18 |
+
|
| 19 |
+
## Steps (Claude, once IP is known — APP_DIR=/opt/themis)
|
| 20 |
+
```bash
|
| 21 |
+
# 0. base packages + Caddy
|
| 22 |
+
apt-get update && apt-get install -y python3 python3-venv python3-pip rsync curl debian-keyring debian-archive-keyring apt-transport-https
|
| 23 |
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
| 24 |
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
| 25 |
+
apt-get update && apt-get install -y caddy
|
| 26 |
+
|
| 27 |
+
# 1. app dir + venv + deps (CPU torch)
|
| 28 |
+
mkdir -p /opt/themis && cd /opt/themis
|
| 29 |
+
python3 -m venv venv
|
| 30 |
+
./venv/bin/pip install -U pip
|
| 31 |
+
./venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu
|
| 32 |
+
./venv/bin/pip install numpy requests fastapi "uvicorn[standard]" clerk-backend-api sentence-transformers rank-bm25
|
| 33 |
+
|
| 34 |
+
# 2. upload from the Mac (run FROM the Mac; rsync is resumable over slow links)
|
| 35 |
+
# rsync -avP phase1/data/thor_artifacts/{escr_vectors.npy,escr_chunks.jsonl,escr_corpus_full.jsonl,escr_meta.jsonl,edges.jsonl,good_law.jsonl,escr_pdfmap.jsonl} USER@IP:/opt/themis/
|
| 36 |
+
# rsync -avP phase1/scripts/{serve.py,clerk_auth.py,frontend.html} USER@IP:/opt/themis/
|
| 37 |
+
# (.env handled separately)
|
| 38 |
+
|
| 39 |
+
# 3. secrets — copy the complete Clerk-enabled .env, then lock it down
|
| 40 |
+
# chmod 600 /opt/themis/.env
|
| 41 |
+
|
| 42 |
+
# 4. systemd service
|
| 43 |
+
cp /opt/themis/themis.service /etc/systemd/system/themis.service # (uploaded from deploy/)
|
| 44 |
+
systemctl daemon-reload && systemctl enable --now themis
|
| 45 |
+
# first start loads 5.8GB → ~30-60s; watch: journalctl -u themis -f (wait for "READY — 37898 judgments")
|
| 46 |
+
|
| 47 |
+
# 5. Caddy (auto-TLS for themis.apexflo.ai)
|
| 48 |
+
cp /opt/themis/Caddyfile /etc/caddy/Caddyfile # (uploaded from deploy/)
|
| 49 |
+
systemctl reload caddy
|
| 50 |
+
|
| 51 |
+
# 6. verify
|
| 52 |
+
curl -s -o /dev/null -w '%{http_code}\n' https://themis.apexflo.ai/ # expect 200 (sign-in shell)
|
| 53 |
+
curl -s https://themis.apexflo.ai/api/v2/auth/config # expect configured=true
|
| 54 |
+
curl -s -o /dev/null -w '%{http_code}\n' https://themis.apexflo.ai/api/search?q=test # expect 401 without bearer token
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Verify checklist
|
| 58 |
+
- [ ] `journalctl -u themis` shows `pdfmap: 37898` + `READY — 37898 judgments, DeepSeek serving=yes`
|
| 59 |
+
- [ ] public auth config reports `configured: true`; protected API returns 401 without a Clerk token
|
| 60 |
+
- [ ] a search streams (SSE through Caddy `flush_interval -1`)
|
| 61 |
+
- [ ] a judgment opens + Official PDF embeds (`/api/pdf` pulls from open registry, caches ≤20)
|
| 62 |
+
- [ ] logs writing to `/opt/themis/logs/usage-*.jsonl` + `internal-*.jsonl`
|
| 63 |
+
- [ ] reboot test: `systemctl reboot`, confirm themis + caddy come back
|
| 64 |
+
|
| 65 |
+
## Rollback / notes
|
| 66 |
+
- Old Thor is gone (reflashed). This box is now the source of truth for *serving*; the Mac holds the artifact backup + git holds the code.
|
| 67 |
+
- To rotate Clerk keys: update `/opt/themis/.env`, then `systemctl restart themis`.
|
| 68 |
+
- The citation-graph rebuild (edges.jsonl) is separate offline work; serving just consumes whatever edges.jsonl is present.
|
phase1/deploy/requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Themis CPU serving deps (no GPU). Install torch from the CPU wheel index separately:
|
| 2 |
+
# pip install torch --index-url https://download.pytorch.org/whl/cpu
|
| 3 |
+
numpy
|
| 4 |
+
requests
|
| 5 |
+
fastapi
|
| 6 |
+
uvicorn[standard]
|
| 7 |
+
clerk-backend-api>=6.0.1,<7
|
| 8 |
+
sentence-transformers
|
| 9 |
+
faiss-cpu
|
| 10 |
+
rank-bm25
|
| 11 |
+
bharat-courts[archive]==0.3.3
|
| 12 |
+
PyMuPDF>=1.24,<2
|
| 13 |
+
python-docx>=1.1,<2
|
| 14 |
+
pytesseract>=0.3.13,<1
|
| 15 |
+
Pillow>=10,<13
|
| 16 |
+
chromadb>=1.5,<2
|
phase1/deploy/themis.service
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[Unit]
|
| 2 |
+
Description=Themis legal-search API (uvicorn)
|
| 3 |
+
After=network-online.target
|
| 4 |
+
Wants=network-online.target
|
| 5 |
+
|
| 6 |
+
[Service]
|
| 7 |
+
Type=simple
|
| 8 |
+
WorkingDirectory=/opt/themis
|
| 9 |
+
# .env holds DEEPSEEK_API_KEY and Clerk backend configuration (chmod 600, never committed)
|
| 10 |
+
EnvironmentFile=/opt/themis/.env
|
| 11 |
+
ExecStart=/opt/themis/venv/bin/uvicorn serve:app --host 127.0.0.1 --port 8000
|
| 12 |
+
Restart=always
|
| 13 |
+
RestartSec=3
|
| 14 |
+
# loading 5.8GB of artifacts takes ~30-60s; don't let systemd kill it as "not started"
|
| 15 |
+
TimeoutStartSec=300
|
| 16 |
+
LimitNOFILE=65536
|
| 17 |
+
|
| 18 |
+
[Install]
|
| 19 |
+
WantedBy=multi-user.target
|
phase1/drafting/templates.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": 1,
|
| 3 |
+
"templates": [
|
| 4 |
+
{
|
| 5 |
+
"id": "slp-civil-full",
|
| 6 |
+
"title": "Special Leave Petition — Civil",
|
| 7 |
+
"description": "Full civil SLP structure with facts, questions of law, grounds and prayers.",
|
| 8 |
+
"filename": "slp_civil_full.pdf",
|
| 9 |
+
"court": "Supreme Court of India",
|
| 10 |
+
"category": "SLP"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"id": "slp-criminal-full",
|
| 14 |
+
"title": "Special Leave Petition — Criminal",
|
| 15 |
+
"description": "Full criminal SLP structure with drafting notes and continuation sections.",
|
| 16 |
+
"filename": "slp_criminal_full.pdf",
|
| 17 |
+
"court": "Supreme Court of India",
|
| 18 |
+
"category": "SLP"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"id": "slp-outline",
|
| 22 |
+
"title": "Special Leave Petition — Outline",
|
| 23 |
+
"description": "A shorter SLP format for an initial working draft.",
|
| 24 |
+
"filename": "slp_outline.pdf",
|
| 25 |
+
"court": "Supreme Court of India",
|
| 26 |
+
"category": "SLP"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"id": "article-32",
|
| 30 |
+
"title": "Article 32 Petition",
|
| 31 |
+
"description": "Petition format for invoking the Supreme Court's writ jurisdiction.",
|
| 32 |
+
"filename": "article_32_petition.pdf",
|
| 33 |
+
"court": "Supreme Court of India",
|
| 34 |
+
"category": "Writ"
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"id": "civil-appeal",
|
| 38 |
+
"title": "Civil Appeal",
|
| 39 |
+
"description": "A concise civil appeal format.",
|
| 40 |
+
"filename": "civil_appeal.pdf",
|
| 41 |
+
"court": "Supreme Court of India",
|
| 42 |
+
"category": "Appeal"
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"id": "curative-petition",
|
| 46 |
+
"title": "Curative Petition",
|
| 47 |
+
"description": "Curative petition structure for the Supreme Court of India.",
|
| 48 |
+
"filename": "curative_petition.pdf",
|
| 49 |
+
"court": "Supreme Court of India",
|
| 50 |
+
"category": "Curative"
|
| 51 |
+
}
|
| 52 |
+
]
|
| 53 |
+
}
|
phase1/eval/.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run.tsv
|
| 2 |
+
run_baseline.tsv
|
| 3 |
+
last_score.json
|
| 4 |
+
sweep_out.txt
|
| 5 |
+
sweep_err.txt
|
| 6 |
+
escr_*.jsonl
|
| 7 |
+
escr_*.jsonl.gz
|
| 8 |
+
*.npy
|
phase1/eval/BENCHMARK.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Themis ⇄ CaseMine search-quality benchmark harness
|
| 2 |
+
|
| 3 |
+
Goal: a defensible, repeatable "Themis vs CaseMine" search-quality score so every change is measured.
|
| 4 |
+
|
| 5 |
+
## Method
|
| 6 |
+
- **Query set**: `bench_queries.json` — 15 realistic, era-neutral queries across intents (fact ×6, issue ×4, vague ×2, citation ×1, casename ×2). Free queries (no fixed gold), since the two systems have different corpora.
|
| 7 |
+
- **Themis side**: `19_benchmark_themis.py` (runs on Thor) — loads the persisted eSCR index once, runs every query through dense(BGE bf16)+BM25+RRF+cross-encoder, saves top-10 per query → `themis_bench_results.json`.
|
| 8 |
+
- **CaseMine side**: collected from `casemine.com/search/in/<query>` in-browser. Result cards = `.listing-card-container`, name link = `.jdlink`. Extractor (run in page context):
|
| 9 |
+
```js
|
| 10 |
+
[...document.querySelectorAll('.listing-card-container')].slice(0,10).map(c=>({
|
| 11 |
+
case_name: c.querySelector('.jdlink')?.textContent?.trim(),
|
| 12 |
+
snippet: c.innerText.replace(/\s+/g,' ').trim().slice(0,300)}))
|
| 13 |
+
```
|
| 14 |
+
→ `casemine_bench_results.json`. (CaseMine search is slow, ~tens of s/query; collect patiently, one navigation per query. Their results are fixed, so collect once and reuse.)
|
| 15 |
+
- **Scoring**: `20_score_benchmark.py` — the isolated relevance reviewer judges each result (query + case + snippet/passage) blind → relevant=1/partial=0.5/not=0 → **relevance@10** per query, per intent, and overall; reports the head-to-head + per-query win/loss.
|
| 16 |
+
|
| 17 |
+
## Baseline data point (demo query, Themis on 8.8k recent-only corpus)
|
| 18 |
+
Query: *"quashing of FIR due to criminal activity included in money diversion of an investor's money"*
|
| 19 |
+
- **Themis relevance@10 = 60%** (2 relevant, 8 partial) — all results on-topic, recent (2021–2025).
|
| 20 |
+
- **CaseMine**: top-10 all clearly on-point (Vikram Doshi, Sushil Suri, Iridium, Bikram Chatterji/Amrapali, B. Rama Raju, Jaypee/63 Moons…) — but they lead with **2010–2014 landmarks + HC cases that are pre-2015, i.e. outside our current corpus window**.
|
| 21 |
+
- **Finding**: the gap on this query is **corpus coverage, not the ranker.** → fix is the full-corpus backfill (1950–2025), after which this becomes a fair algorithm comparison.
|
| 22 |
+
|
| 23 |
+
## Next (post-backfill)
|
| 24 |
+
Re-run `19_benchmark_themis.py` on the full ~41.5k index, collect CaseMine for all 15, run `20_score_benchmark.py` → the real per-intent + overall "Themis vs CaseMine, ±N points" scorecard.
|
phase1/eval/BENCHMARK_3WAY.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Search-quality benchmark — Themis vs CaseMine (Niyam pending)
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-06-24 · **Queries:** 6 (multi-intent subset of bench_queries.json) · **Metric:** blind graded relevance of each system's **top-5 result list**.
|
| 4 |
+
|
| 5 |
+
## Method
|
| 6 |
+
- Each query run on Themis (`/api/search_stream`) and CaseMine (semantic CiteTEXT search), top-5 captured.
|
| 7 |
+
- For each query, the two top-5 lists were **pooled, de-labelled, and shuffled**; a neutral legal-expert judge (one per query, blind to source) scored every candidate **0–3** (3 = leading on-point authority, 2 = relevant, 1 = marginal, 0 = irrelevant / wrong case).
|
| 8 |
+
- nDCG@5 (gain = 2^rel−1), Precision@5 (rel ≥ 2 counts as relevant), mean relevance — computed deterministically. nDCG capped at 1.0.
|
| 9 |
+
- **Fairness note:** I built Themis, so scoring was blind (judge never told which engine produced a result) and used Indian SC legal merit only.
|
| 10 |
+
|
| 11 |
+
## Aggregate (6 queries)
|
| 12 |
+
| Metric | **Themis** | CaseMine |
|
| 13 |
+
|---|---|---|
|
| 14 |
+
| nDCG@5 | **0.77** | 0.64 |
|
| 15 |
+
| Precision@5 | **0.50** | 0.43 |
|
| 16 |
+
| Mean relevance | **1.53** | 1.40 |
|
| 17 |
+
| Query wins (by nDCG) | 3 (fact-1, vague-1, casename-2) | 3 (fact-4, issue-1, issue-3) |
|
| 18 |
+
|
| 19 |
+
Themis is ahead on aggregate, but it's a **genuine 3–3 split**, not a blowout. The pattern is the real story.
|
| 20 |
+
|
| 21 |
+
## Per-query (nDCG@5)
|
| 22 |
+
| Query | intent | Themis | CaseMine | winner |
|
| 23 |
+
|---|---|---|---|---|
|
| 24 |
+
| fact-1 — quash 498A FIR on settlement | fact | **0.97** | 0.19 | **Themis (big)** |
|
| 25 |
+
| fact-4 — anticipatory bail, economic offence | fact | 0.63 | **0.72** | CaseMine |
|
| 26 |
+
| issue-1 — dying declaration sole basis | issue | 0.57 | **0.75** | CaseMine |
|
| 27 |
+
| issue-3 — Art 14 arbitrariness, judicial review | issue | 0.51 | **0.85** | CaseMine |
|
| 28 |
+
| vague-1 — privacy fundamental right / Aadhaar | vague | **1.00** | 0.72 | **Themis** |
|
| 29 |
+
| casename-2 — Vishaka v State of Rajasthan | casename | **0.92** | 0.64 | **Themis** |
|
| 30 |
+
|
| 31 |
+
## What the split means (actionable)
|
| 32 |
+
- **Themis wins the "find the right case fast" queries.** fact-1: Themis put **Gian Singh** + **Jitendra Raghuvanshi** (the two leading authorities on quashing 498A on settlement) at #1–2; CaseMine missed both (top results scored 0–1). vague-1 + casename-2: the new **identity-lookup path** (case-name/citation → metadata) put the right judgment first — casename-2 went from **0 results to a win** after that fix.
|
| 33 |
+
- **CaseMine wins the doctrinal "landmark" queries.** issue-1: CaseMine surfaced **Khushal Rao v State of Bombay** (the foundational dying-declaration authority) at #1 — Themis missed it. issue-3: CaseMine led with **Shrilekha Vidyarthi** + **Om Kumar** (the leading Art-14-arbitrariness authorities); Themis had Om Kumar only at #5. CaseMine's citation-network / CiteTEXT is better at pulling the *old foundational* case for a doctrine.
|
| 34 |
+
|
| 35 |
+
→ **Themis's #1 improvement target: foundational-authority recall on doctrinal/issue queries.** Our dense+rerank favours recent, textually-similar judgments; it under-weights the seminal older case that everyone cites. Candidate fixes: citation-graph signal (boost high-cited-by authorities), or a "leading case" re-rank feature.
|
| 36 |
+
|
| 37 |
+
## Caveats (don't over-read)
|
| 38 |
+
- **2-way, not yet 3-way.** Niyam couldn't be driven in this headless browser — its React search input doesn't fire from synthetic events and keystrokes weren't landing reliably; no results captured. One prior data point: Niyam's exact-name search for "Achin Gupta v State of Haryana" returned a 1980 tax case as #1 (a clear miss). Niyam leg still owed.
|
| 39 |
+
- n = 6 queries, single blind judge per query → **directional, not definitive**. Graded relevance is somewhat subjective.
|
| 40 |
+
- Compares **result lists only** (the fair common denominator). Does not score Themis's grounded answer, CaseMine's AMICUS, or CiteTEXT passages.
|
| 41 |
+
- vague-1: Themis returned multiple Puttaswamy judgments (Privacy + Aadhaar) — all relevant to an Aadhaar-privacy query, but worth a results **dedupe-by-case** pass so near-identical entries don't crowd the top-5.
|
phase1/eval/CP-B_results.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CP-B — Citation graph + good-law accuracy (results, 2026-06-24)
|
| 2 |
+
|
| 3 |
+
Artifacts (`good_law.jsonl`, `edges.jsonl`) are gitignored (large, derived; rebuilt by `21_citator.py`
|
| 4 |
+
on Thor over `escr_corpus_full.jsonl`). This file is the committed, auditable record of the numbers.
|
| 5 |
+
|
| 6 |
+
## Citation graph (`21_citator.py` → `edges.jsonl`)
|
| 7 |
+
- **86,702 directed edges** (63.2k cite + 23.5k name) · **0 self-edges, 0 dangling targets** (every edge → a real corpus doc).
|
| 8 |
+
- **cited_by nonzero coverage: 43%** (baseline before Stage 2 was ~10%).
|
| 9 |
+
- Foundational cases recovered via name-edges: **Khushal Rao 0→37**, Indra Sawhney 90, Vishaka 66, Shrilekha Vidyarthi 18, Maneka Gandhi 247.
|
| 10 |
+
- Build: expanded citation regex (bare/no-vol SCR, Supp., year-first AIR); cross-reporter union-find with **same-year guard**; **name-edges** require a citation cue + are disambiguated by cite-popularity + verified on the second party.
|
| 11 |
+
|
| 12 |
+
## Name-edge precision audit (`37_audit_name_edges.py`)
|
| 13 |
+
| | initial | after cue+popularity | after second-party check |
|
| 14 |
+
|---|---|---|---|
|
| 15 |
+
| name-edges | 57,566 | 29,773 | **23,502** |
|
| 16 |
+
| LLM-judged real-reference | 50% | 53% | **77%** |
|
| 17 |
+
| ambiguity rate (shared-key) | 53% | 45% | **37.5%** |
|
| 18 |
+
|
| 19 |
+
Name-edge noise is **contained**: name-edges do NOT drive good-law (negatives resolve cite-anchored only), and the UI shows no treatment tags — so for search they are a recall booster the cross-encoder vets (a wrong edge costs a wasted re-rank, never a false answer).
|
| 20 |
+
|
| 21 |
+
## Good-law accuracy (`34_eval_goodlaw.py` vs `goodlaw_goldset.json`: 23 overruled + 11 good-law)
|
| 22 |
+
- **Good-law FALSE POSITIVES: 0/7 matched** (Maneka Gandhi, previously falsely "overruled", cleared by the local-Qwen confirmation pass `35_confirm_negatives.py`).
|
| 23 |
+
- **Overruled recall: 2/14 matched** — precision-first per asymmetric cost; the ±170-char Qwen window is the documented recall limiter. (Eval denominator is biased low — single-token landmarks aren't matched; real recall is a smoke test, not calibrated.)
|
| 24 |
+
- Status distribution: unknown 37,794 · overruled 76 (49 after Qwen confirm) · doubted 14→5 · per_incuriam 14→6.
|
| 25 |
+
- **Positive "good law" derivation built (`36_*`) but NOT shipped** — eval showed it falsely cleared 9 known-overruled cases at current recall.
|
| 26 |
+
|
| 27 |
+
## Decisions (founder)
|
| 28 |
+
- **Good-law deferred** — keep only the 0-FP confirmed-negative flags; further good-law work (recall via Tier-2 whole-doc Qwen, positive assertion) is later.
|
| 29 |
+
- **Display**: good-law status shown only for confirmed negatives; **nothing for unknown** (no "not-reviewed" disclaimer); **treatment tags removed** (no false health signal on possibly-overruled cases).
|
| 30 |
+
- **Speed = tier knob** (fast vs deep search) confirmed.
|
| 31 |
+
|
| 32 |
+
## CP-B panel verdict: PASS_WITH_FIXES (wf_383119c2-62d) — fixes applied: false-reassurance treatment tags removed; this evidence file commits the numbers. Deferred: higher overruled recall (Tier-2), hard-mapped eval gold, per_incuriam/partly_overruled coverage.
|
phase1/eval/CP2_extraction_findings.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CP2 — metadata source + extraction findings (validated on a sample)
|
| 2 |
+
|
| 3 |
+
## Source (settled)
|
| 4 |
+
**AWS Open Data: `s3://indian-supreme-court-judgments`** (region `ap-south-1`, `--no-sign-request`).
|
| 5 |
+
- eCourts / **eSCR** (electronic Supreme Court Reports = the *reportable* corpus), **1950–2025**, **~41,539 judgments**.
|
| 6 |
+
- Open, **no CAPTCHA, no scraping, no login**; CC-BY-4.0; refreshed **bi-monthly**; maintained by Dattam Labs.
|
| 7 |
+
- Layout: `metadata/json/year=YYYY/<path>.json` (one per judgment, has a structured `raw_html` eSCR card) · `metadata/parquet/year=YYYY/metadata.parquet` · `data/pdf/year=YYYY/english/<path>_EN.pdf` (the headnoted judgment) · `data/tar/` (bulk).
|
| 8 |
+
- **Decommissioned/unavailable:** digiSCR (`digiscr.sci.gov.in`, NXDOMAIN — merged into SCR & retired) and `main.sci.gov.in`; `scr.sci.gov.in` is live but CAPTCHA-gated. The open S3 dataset replaces all of them.
|
| 9 |
+
|
| 10 |
+
## Extraction (two deterministic layers + one LLM field)
|
| 11 |
+
**Layer 1 — P0 fields from the `raw_html` card (regex/bs4, no LLM, no PDF).** Coverage on a 75-record sample across 2024/2005/1990:
|
| 12 |
+
|
| 13 |
+
| field | overall | 2024 | 2005 | 1990 |
|
| 14 |
+
|---|---|---|---|---|
|
| 15 |
+
| case_name, neutral_citation, equivalent_citations, cnr, reportable, bench, bench_strength, date, case_number, court, year | **100%** | 100% | 100% | 100% |
|
| 16 |
+
| author_judge | 92% | 90% | 100% | 87% |
|
| 17 |
+
| disposition | 91% | 82% | 100% | 100% |
|
| 18 |
+
|
| 19 |
+
> **neutral_citation = 100% across all eras** (eSCR backfilled `YYYY INSC N`). Contrast the HF `sinhal` dataset: 0.9%. This alone fixes the citator's join-key problem the CP2 debate flagged.
|
| 20 |
+
|
| 21 |
+
**Layer 2 — deep/headnote fields by deterministic slice of the PDF headnote** (labeled sections: `Issue for Consideration`, `Held`, `Case Law Cited`, `List of Acts`). Coverage on 15 2024 PDFs:
|
| 22 |
+
|
| 23 |
+
| field | coverage |
|
| 24 |
+
|---|---|
|
| 25 |
+
| issue | 100% |
|
| 26 |
+
| held | 100% |
|
| 27 |
+
| cases_cited (with equivalent citations) | **80%** (avg **5.7 edges/judgment**) |
|
| 28 |
+
| acts (from headnote) | 87% |
|
| 29 |
+
|
| 30 |
+
> `Case Law Cited` parses each precedent **with both its SCR and SCC cites**, e.g. *"State of Goa v. Sanjay Thakran [2007] 3 SCR 507 : (2007) 3 SCC 755"* — the citator's edge list, deterministically. (cases_cited <100% because not every judgment cites prior cases.)
|
| 31 |
+
|
| 32 |
+
**Layer 3 — the only `needs-LLM` field: `treatment`** (relied-on / overruled / distinguished …) classified per cited case from the Held text. Demonstrated with DeepSeek; this is the one human-review-worthy step (feeds `good_law_status`).
|
| 33 |
+
|
| 34 |
+
## What this means
|
| 35 |
+
- Practically the **entire CP2 schema is deterministically extractable** from a free, open, era-robust dataset — only `treatment` needs an LLM.
|
| 36 |
+
- **HF `sinhal` → retired** as the metadata source; eSCR S3 dataset is the production source.
|
| 37 |
+
- Cost axes: P0 = cheap JSON GETs (~41.5k). Deep = PDF download+parse (~41.5k PDFs ≈ tens of GB; use `data/tar/`, ideally run on Thor — in-region to ap-south-1). Embedding/retrieval index is a separate GPU axis.
|
| 38 |
+
|
| 39 |
+
Script: `phase1/scripts/15_extract_escr.py` · sample: `phase1/eval/escr_sample.jsonl`.
|
phase1/eval/CP4_scale_results.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CP4 — per-intent retrieval scorecard AT SCALE (GPU)
|
| 2 |
+
|
| 3 |
+
*255 validated gold queries over a 10,000-doc SC corpus (built on Thor GPU, sentence-transformers BGE-small @ 112 chunks/s). doc-level RRF; rerank = ms-marco cross-encoder over top-40. Retrieval-only (no mq/graded — those need the DeepSeek key, kept off this box).*
|
| 4 |
+
|
| 5 |
+
## top-5 (gold case in first 5 results)
|
| 6 |
+
|
| 7 |
+
| Intent | n | dense | bm25 | hybrid | hybrid+rerank |
|
| 8 |
+
|---|---|---|---|---|---|
|
| 9 |
+
| citation | 40 | 0% | 60% | 0% | 100% |
|
| 10 |
+
| casename_exact | 40 | 57% | 48% | 85% | 100% |
|
| 11 |
+
| casename_fuzzy | 37 | 30% | 19% | 32% | 92% |
|
| 12 |
+
| fact | 44 | 68% | 77% | 75% | 77% |
|
| 13 |
+
| issue | 39 | 51% | 74% | 69% | 72% |
|
| 14 |
+
| vague | 30 | 67% | 90% | 87% | 87% |
|
| 15 |
+
| judge | 25 | 32% | 48% | 48% | 60% |
|
| 16 |
+
|
| 17 |
+
## top-1 / MRR (best pipeline per intent)
|
| 18 |
+
|
| 19 |
+
| Intent | best | top-1 | top-5 | MRR |
|
| 20 |
+
|---|---|---|---|---|
|
| 21 |
+
| citation | **hybrid+rerank** | 90% | 100% | 0.95 |
|
| 22 |
+
| casename_exact | **hybrid+rerank** | 95% | 100% | 0.97 |
|
| 23 |
+
| casename_fuzzy | **hybrid+rerank** | 84% | 92% | 0.87 |
|
| 24 |
+
| fact | **hybrid+rerank** | 64% | 77% | 0.70 |
|
| 25 |
+
| issue | **bm25** | 36% | 74% | 0.52 |
|
| 26 |
+
| vague | **bm25** | 43% | 90% | 0.63 |
|
| 27 |
+
| judge | **hybrid+rerank** | 36% | 60% | 0.47 |
|
| 28 |
+
|
| 29 |
+
## 300 → 10,000 docs: what held, what degraded (apples-to-apples `hybrid+rerank` top-5)
|
| 30 |
+
|
| 31 |
+
| Intent | 300 (CP3) | 10k (CP4) | Δ | read |
|
| 32 |
+
|---|---|---|---|---|
|
| 33 |
+
| citation | 100% | 100% | 0 | **held** — BM25 keeps the exact cite in the top-40 pool, rerank promotes it; my "rerank-rescue collapses at scale" worry did **not** materialise at 10k |
|
| 34 |
+
| casename_exact | 100% | 100% | 0 | **held** |
|
| 35 |
+
| casename_fuzzy | 97% | 92% | −5 | mostly held |
|
| 36 |
+
| fact | 98% | 77% | −21 | degraded |
|
| 37 |
+
| issue | 85% | 72% | −13 | degraded |
|
| 38 |
+
| vague | 97% | 87% | −10 | degraded |
|
| 39 |
+
| judge | 88% | 60% | −28 | degraded most |
|
| 40 |
+
|
| 41 |
+
**Verdict.** Known-item intents (citation / case-name) are **scale-robust** — the lexical+rerank path holds at 33× corpus. Semantic intents **degrade** as distractors grow (expected), with `judge` worst. Two notable scale effects:
|
| 42 |
+
- At 10k, **plain BM25 beats hybrid+rerank** on top-5 for `issue` (74% vs 72%) and `vague` (90% vs 87%) — the cross-encoder slightly *hurts* these at scale. Reranker tuning / candidate-depth is now a real lever.
|
| 43 |
+
- Dense-alone is weak at scale (citation 0%, issue 51%); the value is in the **fusion + rerank**, not dense by itself.
|
| 44 |
+
|
| 45 |
+
### Caveats (don't over-read the semantic drop)
|
| 46 |
+
1. **Retrieval-only run** — no multi-query rewrite and no graded relevance@1 (both need the DeepSeek key, kept off Thor). At 300, `mq` lifted fact/issue and graded relevant@1 was **95–100%** even where exact top-1 was ~74%. So the semantic top-1 here (fact 64%, issue 36%) **understates** real usefulness; the full pipeline would recover much of the gap.
|
| 47 |
+
2. **Embedder differs** — 10k used sentence-transformers BGE-small (GPU); 300 used fastembed BGE-small (same weights, minor numerical differences).
|
| 48 |
+
3. **GPU throughput 112 chunks/s** — works, but that's the *server* cu130 wheel on Thor's integrated Blackwell GPU (compatibility path, not Thor-native kernels). A JetPack-native torch / TensorRT build would be much faster; bulk re-indexing should use that.
|
| 49 |
+
4. Exact-gold is still single-target; on common topics many cases are equally right, so the metric is a floor.
|
phase1/eval/agentic_run.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the agentic controller over a query file -> run.tsv (final ranked docs), for nDCG scoring.
|
| 2 |
+
Two phases: (1) batch-plan all queries in parallel (DeepSeek, cached per qid so ablation runs reuse
|
| 3 |
+
the SAME plans), (2) assemble each query (parallel fetch + rerank + rank) sequentially.
|
| 4 |
+
|
| 5 |
+
Env: THEMIS_DATA, THEMIS_STATUTE, THEMIS_QFILE, OUT, PLANCACHE, THEMIS_ENABLED=all|vector,authority,...
|
| 6 |
+
"""
|
| 7 |
+
import os, sys, json, time
|
| 8 |
+
import concurrent.futures as cf
|
| 9 |
+
import requests
|
| 10 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "scripts"))
|
| 11 |
+
from tools import Corpus
|
| 12 |
+
import agent as A
|
| 13 |
+
|
| 14 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
def _load_env(p):
|
| 16 |
+
for l in open(p):
|
| 17 |
+
l = l.strip()
|
| 18 |
+
if l and not l.startswith("#") and "=" in l:
|
| 19 |
+
k, v = l.split("=", 1); os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
| 20 |
+
_load_env(os.path.join(HERE, "..", "scripts", ".env"))
|
| 21 |
+
HDR = {"Authorization": f"Bearer {os.environ['DEEPSEEK_API_KEY']}", "Content-Type": "application/json"}
|
| 22 |
+
|
| 23 |
+
def llm_fn(msgs):
|
| 24 |
+
for _ in range(3):
|
| 25 |
+
try:
|
| 26 |
+
r = requests.post("https://api.deepseek.com/chat/completions", headers=HDR, timeout=60,
|
| 27 |
+
json={"model": "deepseek-chat", "temperature": 0, "max_tokens": 400, "messages": msgs})
|
| 28 |
+
if r.status_code == 200: return r.json()["choices"][0]["message"]["content"]
|
| 29 |
+
except Exception: time.sleep(2)
|
| 30 |
+
return "{}"
|
| 31 |
+
|
| 32 |
+
QFILE = os.environ.get("THEMIS_QFILE", "authority_queries.tsv")
|
| 33 |
+
OUT = os.environ.get("OUT", "agent_run.tsv")
|
| 34 |
+
PLANCACHE = os.environ.get("PLANCACHE", "plan_" + os.path.basename(QFILE).split(".")[0] + ".json")
|
| 35 |
+
ENABLED = A.ALL_TOOLS if os.environ.get("THEMIS_ENABLED", "all") == "all" else set(os.environ["THEMIS_ENABLED"].split(","))
|
| 36 |
+
ALPHA = float(os.environ.get("ALPHA", "0.3"))
|
| 37 |
+
|
| 38 |
+
rows = []
|
| 39 |
+
for l in open(QFILE, encoding="utf-8"):
|
| 40 |
+
qid, it, t = l.rstrip("\n").split("\t", 2); rows.append((qid, t))
|
| 41 |
+
|
| 42 |
+
# Phase 1: batch-plan (parallel, cached)
|
| 43 |
+
plans = json.load(open(PLANCACHE)) if os.path.exists(PLANCACHE) else {}
|
| 44 |
+
todo = [(qid, t) for qid, t in rows if qid not in plans]
|
| 45 |
+
if todo:
|
| 46 |
+
print(f"planning {len(todo)} queries ...", flush=True); t0 = time.time()
|
| 47 |
+
with cf.ThreadPoolExecutor(max_workers=24) as ex:
|
| 48 |
+
futs = {ex.submit(A.plan, t, llm_fn): qid for qid, t in todo}
|
| 49 |
+
done = 0
|
| 50 |
+
for f in cf.as_completed(futs):
|
| 51 |
+
plans[futs[f]] = f.result(); done += 1
|
| 52 |
+
if done % 50 == 0: json.dump(plans, open(PLANCACHE, "w")); print(f" {done}/{len(todo)} {time.time()-t0:.0f}s", flush=True)
|
| 53 |
+
json.dump(plans, open(PLANCACHE, "w"))
|
| 54 |
+
print(f"plans done {time.time()-t0:.0f}s", flush=True)
|
| 55 |
+
|
| 56 |
+
# Phase 2: assemble (sequential; CE is CPU-bound)
|
| 57 |
+
C = Corpus(os.environ.get("THEMIS_DATA", "."), os.environ.get("THEMIS_STATUTE", "."), device=os.environ.get("THEMIS_DEVICE", "cpu"))
|
| 58 |
+
print(f"assembling {len(rows)} queries (enabled={sorted(ENABLED)}) ...", flush=True)
|
| 59 |
+
t0 = time.time()
|
| 60 |
+
with open(OUT, "w", encoding="utf-8") as f:
|
| 61 |
+
for i, (qid, t) in enumerate(rows):
|
| 62 |
+
ranked, info = A.assemble(C, t, plans[qid], enabled=ENABLED, alpha=ALPHA)
|
| 63 |
+
for rank, d in enumerate(ranked, 1): f.write(f"{qid}\t{rank}\t{d}\n")
|
| 64 |
+
if (i + 1) % 30 == 0: print(f" {i+1}/{len(rows)} {time.time()-t0:.0f}s", flush=True)
|
| 65 |
+
print(f"DONE {len(rows)} -> {OUT} in {time.time()-t0:.0f}s", flush=True)
|
phase1/eval/authority_badlaw.txt
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
2019 INSC 372
|
| 2 |
+
2015 INSC 793
|
| 3 |
+
2015 INSC 52
|
| 4 |
+
2015 INSC 160
|
| 5 |
+
2015 INSC 235
|
| 6 |
+
2015 INSC 257
|
| 7 |
+
2016 INSC 375
|
| 8 |
+
2016 INSC 491
|
| 9 |
+
2016 INSC 526
|
| 10 |
+
2017 INSC 1026
|
| 11 |
+
2017 INSC 1092
|
| 12 |
+
2017 INSC 121
|
| 13 |
+
2017 INSC 102
|
| 14 |
+
2017 INSC 899
|
| 15 |
+
2018 INSC 896
|
| 16 |
+
2018 INSC 732
|
| 17 |
+
2018 INSC 84
|
| 18 |
+
2018 INSC 405
|
| 19 |
+
2018 INSC 714
|
| 20 |
+
2019 INSC 1184
|
| 21 |
+
2019 INSC 511
|
| 22 |
+
2020 INSC 294
|
| 23 |
+
2021 INSC 314
|
| 24 |
+
2022 INSC 1312
|
| 25 |
+
1960 INSC 107
|
| 26 |
+
1960 INSC 123
|
| 27 |
+
1960 INSC 195
|
| 28 |
+
1960 INSC 200
|
| 29 |
+
1961 INSC 177
|
| 30 |
+
1962 INSC 247
|
| 31 |
+
1962 INSC 389
|
| 32 |
+
1962 INSC 328
|
| 33 |
+
1964 INSC 7
|
| 34 |
+
1964 INSC 27
|
| 35 |
+
1964 INSC 203
|
| 36 |
+
1964 INSC 206
|
| 37 |
+
1965 INSC 154
|
| 38 |
+
1966 INSC 155
|
| 39 |
+
1967 INSC 45
|
| 40 |
+
1967 INSC 87
|
| 41 |
+
1967 INSC 122
|
| 42 |
+
1967 INSC 173
|
| 43 |
+
1968 INSC 72
|
| 44 |
+
1969 INSC 8
|
| 45 |
+
1969 INSC 20
|
| 46 |
+
1969 INSC 77
|
| 47 |
+
1969 INSC 99
|
| 48 |
+
1969 INSC 87
|
| 49 |
+
1970 INSC 18
|
| 50 |
+
1970 INSC 190
|
| 51 |
+
1971 INSC 20
|
| 52 |
+
1974 INSC 256
|
| 53 |
+
1975 INSC 212
|
| 54 |
+
1976 INSC 270
|
| 55 |
+
1976 INSC 231
|
| 56 |
+
1976 INSC 250
|
| 57 |
+
1976 INSC 272
|
| 58 |
+
1977 INSC 28
|
| 59 |
+
1977 INSC 75
|
| 60 |
+
1977 INSC 155
|
| 61 |
+
1978 INSC 16
|
| 62 |
+
1981 INSC 175
|
| 63 |
+
1981 INSC 211
|
| 64 |
+
1981 INSC 209
|
| 65 |
+
1983 INSC 10
|
| 66 |
+
1984 INSC 67
|
| 67 |
+
1984 INSC 152
|
| 68 |
+
1985 INSC 101
|
| 69 |
+
1987 INSC 259
|
| 70 |
+
1988 INSC 61
|
| 71 |
+
1989 INSC 54
|
| 72 |
+
1996 INSC 90
|
| 73 |
+
1996 INSC 800
|
| 74 |
+
1997 INSC 441
|
| 75 |
+
1998 INSC 185
|
| 76 |
+
2001 INSC 158
|
| 77 |
+
2002 INSC 66
|
| 78 |
+
2002 INSC 123
|
| 79 |
+
2004 INSC 34
|
| 80 |
+
2004 INSC 182
|
| 81 |
+
2005 INSC 146
|
| 82 |
+
2007 INSC 1026
|
| 83 |
+
2007 INSC 28
|
| 84 |
+
2007 INSC 475
|
| 85 |
+
2007 INSC 772
|
| 86 |
+
2008 INSC 930
|
| 87 |
+
2008 INSC 82
|
| 88 |
+
2008 INSC 677
|
| 89 |
+
2009 INSC 946
|
| 90 |
+
2009 INSC 1045
|
| 91 |
+
2009 INSC 1195
|
| 92 |
+
2009 INSC 209
|
| 93 |
+
2010 INSC 69
|
| 94 |
+
2010 INSC 177
|
| 95 |
+
2011 INSC 508
|
| 96 |
+
2012 INSC 49
|
| 97 |
+
2013 INSC 830
|
| 98 |
+
2013 INSC 684
|
| 99 |
+
2013 INSC 823
|
| 100 |
+
2013 INSC 377
|
| 101 |
+
2013 INSC 494
|
| 102 |
+
2014 INSC 218
|
| 103 |
+
2014 INSC 617
|
| 104 |
+
2014 INSC 579
|
phase1/eval/authority_qrels.tsv
ADDED
|
@@ -0,0 +1,1486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
1 2006 INSC 216 3
|
| 2 |
+
1 2015 INSC 792 2
|
| 3 |
+
1 2016 INSC 1215 2
|
| 4 |
+
1 2017 INSC 1101 2
|
| 5 |
+
1 2017 INSC 81 2
|
| 6 |
+
1 2018 INSC 1123 2
|
| 7 |
+
1 2018 INSC 190 2
|
| 8 |
+
1 2018 INSC 258 2
|
| 9 |
+
1 2019 INSC 983 2
|
| 10 |
+
1 2019 INSC 1373 2
|
| 11 |
+
1 2019 INSC 1157 2
|
| 12 |
+
2 1955 INSC 36 3
|
| 13 |
+
2 2018 INSC 740 2
|
| 14 |
+
2 2019 INSC 976 2
|
| 15 |
+
2 2020 INSC 548 2
|
| 16 |
+
2 2021 INSC 624 2
|
| 17 |
+
2 2021 INSC 324 2
|
| 18 |
+
2 2022 INSC 506 2
|
| 19 |
+
2 2023 INSC 123 2
|
| 20 |
+
2 1958 INSC 43 2
|
| 21 |
+
2 1961 INSC 90 2
|
| 22 |
+
2 1961 INSC 161 2
|
| 23 |
+
3 2020 INSC 294 3
|
| 24 |
+
3 2020 INSC 135 2
|
| 25 |
+
3 2021 INSC 43 2
|
| 26 |
+
3 2021 INSC 711 2
|
| 27 |
+
3 2022 INSC 884 2
|
| 28 |
+
3 2022 INSC 362 2
|
| 29 |
+
3 2022 INSC 1190 2
|
| 30 |
+
3 2022 INSC 1230 2
|
| 31 |
+
3 2022 INSC 1231 2
|
| 32 |
+
3 2022 INSC 1248 2
|
| 33 |
+
3 2022 INSC 1245 2
|
| 34 |
+
4 1952 INSC 1 3
|
| 35 |
+
4 2017 INSC 801 2
|
| 36 |
+
4 2020 INSC 432 2
|
| 37 |
+
4 2021 INSC 28 2
|
| 38 |
+
4 2021 INSC 777 2
|
| 39 |
+
4 2022 INSC 1085 2
|
| 40 |
+
4 2023 INSC 144 2
|
| 41 |
+
4 2025 INSC 593 2
|
| 42 |
+
4 1953 INSC 51 2
|
| 43 |
+
4 1958 INSC 101 2
|
| 44 |
+
4 1988 INSC 123 2
|
| 45 |
+
5 2009 INSC 506 3
|
| 46 |
+
5 2015 INSC 420 2
|
| 47 |
+
5 2016 INSC 909 2
|
| 48 |
+
5 2017 INSC 1150 2
|
| 49 |
+
5 2017 INSC 1068 2
|
| 50 |
+
5 2017 INSC 614 2
|
| 51 |
+
5 2018 INSC 738 2
|
| 52 |
+
5 2018 INSC 967 2
|
| 53 |
+
5 2018 INSC 43 2
|
| 54 |
+
5 2018 INSC 42 2
|
| 55 |
+
5 2018 INSC 126 2
|
| 56 |
+
6 1994 INSC 112 3
|
| 57 |
+
6 2015 INSC 120 2
|
| 58 |
+
6 2015 INSC 257 2
|
| 59 |
+
6 2015 INSC 455 2
|
| 60 |
+
6 2016 INSC 630 2
|
| 61 |
+
6 2017 INSC 960 2
|
| 62 |
+
6 2017 INSC 406 2
|
| 63 |
+
6 2018 INSC 165 2
|
| 64 |
+
6 2018 INSC 282 2
|
| 65 |
+
6 2018 INSC 266 2
|
| 66 |
+
6 2018 INSC 248 2
|
| 67 |
+
7 2002 INSC 454 3
|
| 68 |
+
7 2016 INSC 318 2
|
| 69 |
+
7 2014 INSC 623 2
|
| 70 |
+
7 2019 INSC 1081 2
|
| 71 |
+
7 2020 INSC 557 2
|
| 72 |
+
7 2021 INSC 551 2
|
| 73 |
+
7 2022 INSC 1175 2
|
| 74 |
+
7 2023 INSC 147 2
|
| 75 |
+
7 2024_11_1647_2038 2
|
| 76 |
+
7 2025 INSC 1063 2
|
| 77 |
+
7 2007 INSC 198 2
|
| 78 |
+
8 2014 INSC 53 3
|
| 79 |
+
8 2015 INSC 179 2
|
| 80 |
+
8 2016 INSC 1197 2
|
| 81 |
+
8 2018 INSC 115 2
|
| 82 |
+
8 2019 INSC 1184 2
|
| 83 |
+
8 2022 INSC 362 2
|
| 84 |
+
8 2022 INSC 1190 2
|
| 85 |
+
8 2022 INSC 1230 2
|
| 86 |
+
8 2022 INSC 1231 2
|
| 87 |
+
8 2022 INSC 1248 2
|
| 88 |
+
8 2022 INSC 1245 2
|
| 89 |
+
9 2007 INSC 142 3
|
| 90 |
+
9 2016 INSC 217 2
|
| 91 |
+
9 2018 INSC 705 2
|
| 92 |
+
9 2018 INSC 833 2
|
| 93 |
+
9 2018 INSC 594 2
|
| 94 |
+
9 2019 INSC 1363 2
|
| 95 |
+
9 2019 INSC 84 2
|
| 96 |
+
9 2019 INSC 481 2
|
| 97 |
+
9 2020 INSC 675 2
|
| 98 |
+
9 2020 INSC 563 2
|
| 99 |
+
9 2021 INSC 60 2
|
| 100 |
+
10 1994 INSC 283 3
|
| 101 |
+
10 2016 INSC 273 2
|
| 102 |
+
10 2016 INSC 516 2
|
| 103 |
+
10 2018 INSC 115 2
|
| 104 |
+
10 2020 INSC 399 2
|
| 105 |
+
10 2021 INSC 326 2
|
| 106 |
+
10 2021 INSC 492 2
|
| 107 |
+
10 2022 INSC 1208 2
|
| 108 |
+
10 2023 INSC 324 2
|
| 109 |
+
10 2023 INSC 760 2
|
| 110 |
+
10 2024 INSC 904 2
|
| 111 |
+
11 1998 INSC 183 3
|
| 112 |
+
11 2017 INSC 1269 2
|
| 113 |
+
11 2018 INSC 862 2
|
| 114 |
+
11 2018 INSC 248 2
|
| 115 |
+
11 2019 INSC 1099 2
|
| 116 |
+
11 2019 INSC 1102 2
|
| 117 |
+
11 2019 INSC 1248 2
|
| 118 |
+
11 2019 INSC 99 2
|
| 119 |
+
11 2019 INSC 679 2
|
| 120 |
+
11 2020 INSC 373 2
|
| 121 |
+
11 2020 INSC 489 2
|
| 122 |
+
12 2000 INSC 339 3
|
| 123 |
+
12 2017_2_779_787 2
|
| 124 |
+
12 2018 INSC 446 2
|
| 125 |
+
12 2018 INSC 522 2
|
| 126 |
+
12 2019 INSC 743 2
|
| 127 |
+
12 2019 INSC 815 2
|
| 128 |
+
12 2020 INSC 674 2
|
| 129 |
+
12 2020 INSC 576 2
|
| 130 |
+
12 2021 INSC 591 2
|
| 131 |
+
12 2021 INSC 389 2
|
| 132 |
+
12 2022 INSC 1024 2
|
| 133 |
+
13 2006 INSC 711 3
|
| 134 |
+
13 2016 INSC 1174 2
|
| 135 |
+
13 2016 INSC 482 2
|
| 136 |
+
13 2017 INSC 801 2
|
| 137 |
+
13 2017 INSC 88 2
|
| 138 |
+
13 2017 INSC 121 2
|
| 139 |
+
13 2018 INSC 223 2
|
| 140 |
+
13 2019 INSC 312 2
|
| 141 |
+
13 2019 INSC 671 2
|
| 142 |
+
13 2020 INSC 344 2
|
| 143 |
+
13 2025 INSC 1101 2
|
| 144 |
+
14 1997 INSC 288 3
|
| 145 |
+
14 2015 INSC 217 2
|
| 146 |
+
14 2017 INSC 768 2
|
| 147 |
+
14 2018 INSC 282 2
|
| 148 |
+
14 2018 INSC 456 2
|
| 149 |
+
14 2019 INSC 823 2
|
| 150 |
+
14 2018 INSC 880 2
|
| 151 |
+
14 2019 INSC 1236 2
|
| 152 |
+
14 2019 INSC 1353 2
|
| 153 |
+
14 2019 INSC 1233 2
|
| 154 |
+
14 2019 INSC 220 2
|
| 155 |
+
15 1960 INSC 61 3
|
| 156 |
+
15 2016 INSC 851 2
|
| 157 |
+
15 2018 INSC 246 2
|
| 158 |
+
15 2021 INSC 177 2
|
| 159 |
+
15 2021 INSC 253 2
|
| 160 |
+
15 2022 INSC 1294 2
|
| 161 |
+
15 2022 INSC 827 2
|
| 162 |
+
15 2023 INSC 189 2
|
| 163 |
+
15 2024 INSC 897 2
|
| 164 |
+
15 2025 INSC 596 2
|
| 165 |
+
15 2025 INSC 767 2
|
| 166 |
+
16 2008 INSC 853 3
|
| 167 |
+
16 2015 INSC 886 2
|
| 168 |
+
16 2017 INSC 1012 2
|
| 169 |
+
16 2018 INSC 997 2
|
| 170 |
+
16 2018 INSC 1194 2
|
| 171 |
+
16 2019 INSC 1116 2
|
| 172 |
+
16 2019 INSC 1107 2
|
| 173 |
+
16 2019 INSC 851 2
|
| 174 |
+
16 2019 INSC 518 2
|
| 175 |
+
16 2019 INSC 196 2
|
| 176 |
+
16 2020 INSC 624 2
|
| 177 |
+
17 2005 INSC 526 3
|
| 178 |
+
17 2016 INSC 454 2
|
| 179 |
+
17 2019 INSC 1292 2
|
| 180 |
+
17 2019 INSC 1299 2
|
| 181 |
+
17 2019 INSC 511 2
|
| 182 |
+
17 2021 INSC 12 2
|
| 183 |
+
17 2023 INSC 423 2
|
| 184 |
+
17 2024 INSC 155 2
|
| 185 |
+
17 2024 INSC 710 2
|
| 186 |
+
17 2007 INSC 463 2
|
| 187 |
+
17 2007 INSC 826 2
|
| 188 |
+
18 2019 INSC 95 3
|
| 189 |
+
18 2019 INSC 889 2
|
| 190 |
+
18 2019 INSC 1289 2
|
| 191 |
+
18 2019 INSC 1256 2
|
| 192 |
+
18 2020 INSC 490 2
|
| 193 |
+
18 2020 INSC 699 2
|
| 194 |
+
18 2020 INSC 264 2
|
| 195 |
+
18 2020 INSC 227 2
|
| 196 |
+
18 2021 INSC 590 2
|
| 197 |
+
18 2021 INSC 828 2
|
| 198 |
+
18 2021 INSC 133 2
|
| 199 |
+
19 2017 INSC 1068 3
|
| 200 |
+
19 2018 INSC 828 2
|
| 201 |
+
19 2018 INSC 967 2
|
| 202 |
+
19 2018 INSC 679 2
|
| 203 |
+
19 2019 INSC 912 2
|
| 204 |
+
19 2019 INSC 1341 2
|
| 205 |
+
19 2019 INSC 1348 2
|
| 206 |
+
19 2019 INSC 200 2
|
| 207 |
+
19 2019 INSC 489 2
|
| 208 |
+
19 2019 INSC 668 2
|
| 209 |
+
19 2020 INSC 535 2
|
| 210 |
+
20 2017 INSC 801 3
|
| 211 |
+
20 2018 INSC 898 2
|
| 212 |
+
20 2018 INSC 1201 2
|
| 213 |
+
20 2018 INSC 223 2
|
| 214 |
+
20 2019 INSC 855 2
|
| 215 |
+
20 2018 INSC 880 2
|
| 216 |
+
20 2019 INSC 915 2
|
| 217 |
+
20 2014 INSC 623 2
|
| 218 |
+
20 2019 INSC 1233 2
|
| 219 |
+
20 2019 INSC 52 2
|
| 220 |
+
20 2020 INSC 572 2
|
| 221 |
+
21 2004 INSC 256 3
|
| 222 |
+
21 2018 INSC 1153 2
|
| 223 |
+
21 2019 INSC 724 2
|
| 224 |
+
21 2020 INSC 549 2
|
| 225 |
+
21 2020 INSC 652 2
|
| 226 |
+
21 2021 INSC 642 2
|
| 227 |
+
21 2021 INSC 133 2
|
| 228 |
+
21 2021 INSC 430 2
|
| 229 |
+
21 2022 INSC 807 2
|
| 230 |
+
21 2011 INSC 162 2
|
| 231 |
+
21 2013 INSC 811 2
|
| 232 |
+
22 2014 INSC 229 3
|
| 233 |
+
22 2015 INSC 218 2
|
| 234 |
+
22 2018 INSC 820 2
|
| 235 |
+
22 2018 INSC 248 2
|
| 236 |
+
22 2019 INSC 1333 2
|
| 237 |
+
22 2019 INSC 611 2
|
| 238 |
+
22 2017 INSC 64 2
|
| 239 |
+
22 2020 INSC 524 2
|
| 240 |
+
22 2021 INSC 614 2
|
| 241 |
+
22 2021 INSC 304 2
|
| 242 |
+
22 2021 INSC 643 2
|
| 243 |
+
23 1997 INSC 604 3
|
| 244 |
+
23 2017 INSC 801 2
|
| 245 |
+
23 2018 INSC 223 2
|
| 246 |
+
23 2018 INSC 790 2
|
| 247 |
+
23 2020 INSC 355 2
|
| 248 |
+
23 2023 INSC 920 2
|
| 249 |
+
23 2023 INSC 190 2
|
| 250 |
+
23 2025 INSC 118 2
|
| 251 |
+
23 2014 INSC 894 2
|
| 252 |
+
23 2014 INSC 46 2
|
| 253 |
+
23 2014 INSC 275 2
|
| 254 |
+
24 1958 INSC 18 3
|
| 255 |
+
24 2015 INSC 85 2
|
| 256 |
+
24 2015 INSC 425 2
|
| 257 |
+
24 2016 INSC 65 2
|
| 258 |
+
24 2016 INSC 939 2
|
| 259 |
+
24 2020 INSC 547 2
|
| 260 |
+
24 2020 INSC 540 2
|
| 261 |
+
24 2021 INSC 802 2
|
| 262 |
+
24 2022 INSC 153 2
|
| 263 |
+
24 2023 INSC 79 2
|
| 264 |
+
24 1974 INSC 218 2
|
| 265 |
+
25 1955 INSC 27 3
|
| 266 |
+
25 2016 INSC 934 2
|
| 267 |
+
25 2016 INSC 526 2
|
| 268 |
+
25 2018 INSC 728 2
|
| 269 |
+
25 2018 INSC 593 2
|
| 270 |
+
25 2019 INSC 947 2
|
| 271 |
+
25 2018 INSC 880 2
|
| 272 |
+
25 2019 INSC 915 2
|
| 273 |
+
25 2020 INSC 508 2
|
| 274 |
+
25 2020 INSC 158 2
|
| 275 |
+
25 2022 INSC 1085 2
|
| 276 |
+
26 1957 INSC 38 3
|
| 277 |
+
26 2017 INSC 1268 2
|
| 278 |
+
26 2019 INSC 851 2
|
| 279 |
+
26 2020 INSC 624 2
|
| 280 |
+
26 2021 INSC 443 2
|
| 281 |
+
26 2022 INSC 670 2
|
| 282 |
+
26 2022 INSC 637 2
|
| 283 |
+
26 2023 INSC 269 2
|
| 284 |
+
26 2024 INSC 312 2
|
| 285 |
+
26 2025 INSC 936 2
|
| 286 |
+
26 2008 INSC 856 2
|
| 287 |
+
27 1960 INSC 221 3
|
| 288 |
+
27 2018 INSC 241 2
|
| 289 |
+
27 2020 INSC 355 2
|
| 290 |
+
27 2020 INSC 428 2
|
| 291 |
+
27 2020 INSC 264 2
|
| 292 |
+
27 2020 INSC 294 2
|
| 293 |
+
27 2020 INSC 633 2
|
| 294 |
+
27 2022 INSC 841 2
|
| 295 |
+
27 2022 INSC 3 2
|
| 296 |
+
27 2025 INSC 124 2
|
| 297 |
+
27 1967 INSC 172 2
|
| 298 |
+
28 2010 INSC 219 3
|
| 299 |
+
28 2015 INSC 828 2
|
| 300 |
+
28 2015 INSC 419 2
|
| 301 |
+
28 2016 INSC 384 2
|
| 302 |
+
28 2016 INSC 943 2
|
| 303 |
+
28 2017 INSC 250 2
|
| 304 |
+
28 2019 INSC 1346 2
|
| 305 |
+
28 2019 INSC 1303 2
|
| 306 |
+
28 2020 INSC 539 2
|
| 307 |
+
28 2022 INSC 807 2
|
| 308 |
+
28 2022 INSC 1177 2
|
| 309 |
+
29 2014 INSC 21 3
|
| 310 |
+
29 2016 INSC 401 2
|
| 311 |
+
29 2017 INSC 999 2
|
| 312 |
+
29 2017 INSC 1201 2
|
| 313 |
+
29 2018 INSC 282 2
|
| 314 |
+
29 2019 INSC 798 2
|
| 315 |
+
29 2019 INSC 1161 2
|
| 316 |
+
29 2019 INSC 1355 2
|
| 317 |
+
29 2019 INSC 1146 2
|
| 318 |
+
29 2019 INSC 1303 2
|
| 319 |
+
29 2019 INSC 371 2
|
| 320 |
+
30 1989 INSC 192 3
|
| 321 |
+
30 2015 INSC 76 2
|
| 322 |
+
30 2018 INSC 248 2
|
| 323 |
+
30 2020 INSC 512 2
|
| 324 |
+
30 2022 INSC 975 2
|
| 325 |
+
30 2022 INSC 780 2
|
| 326 |
+
30 2024 INSC 13 2
|
| 327 |
+
30 2025 INSC 249 2
|
| 328 |
+
30 1992 INSC 171 2
|
| 329 |
+
30 1994 INSC 380 2
|
| 330 |
+
30 1994 INSC 478 2
|
| 331 |
+
31 1958 INSC 17 3
|
| 332 |
+
31 2016 INSC 1019 2
|
| 333 |
+
31 2019 INSC 734 2
|
| 334 |
+
31 2021 INSC 659 2
|
| 335 |
+
31 2022 INSC 331 2
|
| 336 |
+
31 2023 INSC 817 2
|
| 337 |
+
31 2025 INSC 757 2
|
| 338 |
+
31 1960 INSC 190 2
|
| 339 |
+
31 1961 INSC 120 2
|
| 340 |
+
31 1971 INSC 280 2
|
| 341 |
+
31 1977 INSC 177 2
|
| 342 |
+
32 2007 INSC 28 3
|
| 343 |
+
32 2017 INSC 801 2
|
| 344 |
+
32 2017 INSC 768 2
|
| 345 |
+
32 2018 INSC 881 2
|
| 346 |
+
32 2018 INSC 880 2
|
| 347 |
+
32 2019 INSC 1102 2
|
| 348 |
+
32 2019 INSC 1007 2
|
| 349 |
+
32 2020 INSC 512 2
|
| 350 |
+
32 2020 INSC 344 2
|
| 351 |
+
32 2021 INSC 434 2
|
| 352 |
+
32 2021 INSC 340 2
|
| 353 |
+
33 2018 INSC 115 3
|
| 354 |
+
33 2019 INSC 678 2
|
| 355 |
+
34 2009 INSC 808 3
|
| 356 |
+
34 2018 INSC 1193 2
|
| 357 |
+
34 2018 INSC 1112 2
|
| 358 |
+
34 2018 INSC 1194 2
|
| 359 |
+
34 2019 INSC 247 2
|
| 360 |
+
34 2019 INSC 1107 2
|
| 361 |
+
34 2019 INSC 851 2
|
| 362 |
+
34 2019 INSC 518 2
|
| 363 |
+
34 2019 INSC 196 2
|
| 364 |
+
34 2020 INSC 624 2
|
| 365 |
+
34 2022 INSC 52 2
|
| 366 |
+
35 1956 INSC 28 3
|
| 367 |
+
35 2019 INSC 1224 2
|
| 368 |
+
35 2020 INSC 382 2
|
| 369 |
+
35 2021 INSC 115 2
|
| 370 |
+
35 2021 INSC 283 2
|
| 371 |
+
35 2022 INSC 752 2
|
| 372 |
+
35 2022 INSC 545 2
|
| 373 |
+
35 2024 INSC 812 2
|
| 374 |
+
35 1995 INSC 212 2
|
| 375 |
+
35 1996 INSC 419 2
|
| 376 |
+
35 1997 INSC 43 2
|
| 377 |
+
36 1957 INSC 35 3
|
| 378 |
+
36 2015 INSC 257 2
|
| 379 |
+
36 2016 INSC 955 2
|
| 380 |
+
36 2017 INSC 658 2
|
| 381 |
+
36 2019 INSC 1236 2
|
| 382 |
+
36 2021 INSC 92 2
|
| 383 |
+
36 2021 INSC 340 2
|
| 384 |
+
36 2022 INSC 331 2
|
| 385 |
+
36 2023 INSC 81 2
|
| 386 |
+
36 1963 INSC 202 2
|
| 387 |
+
36 1982 INSC 58 2
|
| 388 |
+
37 1993 INSC 316 3
|
| 389 |
+
37 2019 INSC 529 2
|
| 390 |
+
37 2019 INSC 764 2
|
| 391 |
+
37 2020 INSC 93 2
|
| 392 |
+
37 2023 INSC 975 2
|
| 393 |
+
37 2023 INSC 11 2
|
| 394 |
+
37 2025 INSC 555 2
|
| 395 |
+
37 2025 INSC 742 2
|
| 396 |
+
37 2025 INSC 997 2
|
| 397 |
+
37 2003 INSC 442 2
|
| 398 |
+
37 2010 INSC 90 2
|
| 399 |
+
38 2001 INSC 80 3
|
| 400 |
+
38 2017 INSC 802 2
|
| 401 |
+
38 2018 INSC 850 2
|
| 402 |
+
38 2020 INSC 185 2
|
| 403 |
+
38 2020 INSC 173 2
|
| 404 |
+
38 2020 INSC 511 2
|
| 405 |
+
38 2021 INSC 862 2
|
| 406 |
+
38 2022 INSC 642 2
|
| 407 |
+
38 2022 INSC 433 2
|
| 408 |
+
38 2022 INSC 997 2
|
| 409 |
+
38 2007 INSC 932 2
|
| 410 |
+
39 2013 INSC 748 3
|
| 411 |
+
39 2018 INSC 820 2
|
| 412 |
+
39 2018 INSC 1039 2
|
| 413 |
+
39 2018 INSC 549 2
|
| 414 |
+
39 2018 INSC 248 2
|
| 415 |
+
39 2019 INSC 1102 2
|
| 416 |
+
39 2019 INSC 1333 2
|
| 417 |
+
39 2019 INSC 1242 2
|
| 418 |
+
39 2019 INSC 611 2
|
| 419 |
+
39 2020 INSC 682 2
|
| 420 |
+
39 2020 INSC 432 2
|
| 421 |
+
40 1996 INSC 419 3
|
| 422 |
+
40 2015 INSC 912 2
|
| 423 |
+
40 2017 INSC 1014 2
|
| 424 |
+
40 2017 INSC 478 2
|
| 425 |
+
40 2018 INSC 880 2
|
| 426 |
+
40 2020 INSC 382 2
|
| 427 |
+
40 2020 INSC 350 2
|
| 428 |
+
40 2023 INSC 324 2
|
| 429 |
+
40 2007 INSC 370 2
|
| 430 |
+
41 1960 INSC 100 3
|
| 431 |
+
41 2015 INSC 906 2
|
| 432 |
+
41 2022 INSC 506 2
|
| 433 |
+
41 1964 INSC 209 2
|
| 434 |
+
41 1968 INSC 72 2
|
| 435 |
+
41 1968 INSC 268 2
|
| 436 |
+
41 1995 INSC 328 2
|
| 437 |
+
41 2010 INSC 124 2
|
| 438 |
+
41 2011 INSC 555 2
|
| 439 |
+
41 2011 INSC 635 2
|
| 440 |
+
41 2004 INSC 203 2
|
| 441 |
+
42 2002 INSC 253 3
|
| 442 |
+
42 2016 INSC 289 2
|
| 443 |
+
42 2018 INSC 862 2
|
| 444 |
+
42 2018 INSC 164 2
|
| 445 |
+
42 2019 INSC 1103 2
|
| 446 |
+
42 2019 INSC 1233 2
|
| 447 |
+
42 2019 INSC 210 2
|
| 448 |
+
42 2022 INSC 958 2
|
| 449 |
+
42 2023 INSC 499 2
|
| 450 |
+
42 2024 INSC 30 2
|
| 451 |
+
42 2024 INSC 113 2
|
| 452 |
+
43 1957 INSC 10 3
|
| 453 |
+
43 2015 INSC 966 2
|
| 454 |
+
43 2018 INSC 969 2
|
| 455 |
+
43 2019 INSC 688 2
|
| 456 |
+
43 2021 INSC 189 2
|
| 457 |
+
43 2022 INSC 188 2
|
| 458 |
+
43 2025 INSC 1024 2
|
| 459 |
+
43 1960 INSC 86 2
|
| 460 |
+
43 1960 INSC 87 2
|
| 461 |
+
43 2010 INSC 730 2
|
| 462 |
+
43 2013 INSC 670 2
|
| 463 |
+
44 1952 INSC 2 3
|
| 464 |
+
44 2015 INSC 912 2
|
| 465 |
+
44 2016_2_65_70 2
|
| 466 |
+
44 2019 INSC 1236 2
|
| 467 |
+
44 2021 INSC 179 2
|
| 468 |
+
44 2023 INSC 190 2
|
| 469 |
+
44 1975 INSC 134 2
|
| 470 |
+
44 1975 INSC 214 2
|
| 471 |
+
44 1977 INSC 227 2
|
| 472 |
+
44 1988 INSC 46 2
|
| 473 |
+
44 1994 INSC 111 2
|
| 474 |
+
45 2005 INSC 129 3
|
| 475 |
+
45 2016 INSC 1056 2
|
| 476 |
+
45 2017 INSC 1074 2
|
| 477 |
+
45 2017 INSC 776 2
|
| 478 |
+
45 2017 INSC 658 2
|
| 479 |
+
45 2018 INSC 214 2
|
| 480 |
+
45 2019 INSC 636 2
|
| 481 |
+
45 2020 INSC 531 2
|
| 482 |
+
45 2021 INSC 180 2
|
| 483 |
+
45 2023 INSC 724 2
|
| 484 |
+
45 2024 INSC 260 2
|
| 485 |
+
46 2017 INSC 452 3
|
| 486 |
+
46 2018 INSC 898 2
|
| 487 |
+
46 2018 INSC 455 2
|
| 488 |
+
46 2018 INSC 790 2
|
| 489 |
+
46 2019 INSC 889 2
|
| 490 |
+
46 2018 INSC 880 2
|
| 491 |
+
46 2019 INSC 457 2
|
| 492 |
+
46 2020 INSC 707 2
|
| 493 |
+
46 2023 INSC 29 2
|
| 494 |
+
46 2024 INSC 751 2
|
| 495 |
+
46 2024 INSC 113 2
|
| 496 |
+
47 2004 INSC 244 3
|
| 497 |
+
47 2015 INSC 942 2
|
| 498 |
+
47 2017 INSC 976 2
|
| 499 |
+
47 2017 INSC 388 2
|
| 500 |
+
47 2017 INSC 355 2
|
| 501 |
+
47 2018 INSC 1034 2
|
| 502 |
+
47 2018 INSC 200 2
|
| 503 |
+
47 2018 INSC 241 2
|
| 504 |
+
47 2019 INSC 889 2
|
| 505 |
+
47 2019 INSC 148 2
|
| 506 |
+
47 2019 INSC 707 2
|
| 507 |
+
48 1999 INSC 282 3
|
| 508 |
+
48 2015 INSC 341 2
|
| 509 |
+
48 2018 INSC 426 2
|
| 510 |
+
48 2019 INSC 1145 2
|
| 511 |
+
48 2020 INSC 620 2
|
| 512 |
+
48 2020 INSC 197 2
|
| 513 |
+
48 2020 INSC 524 2
|
| 514 |
+
48 2023 INSC 878 2
|
| 515 |
+
48 2025 INSC 1045 2
|
| 516 |
+
48 2025 INSC 1090 2
|
| 517 |
+
48 2025 INSC 1111 2
|
| 518 |
+
49 2012 INSC 428 3
|
| 519 |
+
49 2018 INSC 1018 2
|
| 520 |
+
49 2018 INSC 110 2
|
| 521 |
+
49 2018 INSC 455 2
|
| 522 |
+
49 2019 INSC 799 2
|
| 523 |
+
49 2023 INSC 607 2
|
| 524 |
+
49 2023 INSC 7 2
|
| 525 |
+
49 2025 INSC 255 2
|
| 526 |
+
49 2014 INSC 562 2
|
| 527 |
+
49 2014 INSC 294 2
|
| 528 |
+
50 2003 INSC 176 3
|
| 529 |
+
50 2015 INSC 912 2
|
| 530 |
+
50 2015 INSC 942 2
|
| 531 |
+
50 2018 INSC 164 2
|
| 532 |
+
50 2018 INSC 248 2
|
| 533 |
+
50 2018 INSC 880 2
|
| 534 |
+
50 2019 INSC 1237 2
|
| 535 |
+
50 2019 INSC 1233 2
|
| 536 |
+
50 2021 INSC 388 2
|
| 537 |
+
50 2023 INSC 4 2
|
| 538 |
+
50 2024 INSC 113 2
|
| 539 |
+
51 1993 INSC 40 3
|
| 540 |
+
51 2017 INSC 801 2
|
| 541 |
+
51 2018 INSC 1201 2
|
| 542 |
+
51 2018 INSC 880 2
|
| 543 |
+
51 2023 INSC 190 2
|
| 544 |
+
51 2025 INSC 1063 2
|
| 545 |
+
51 1995 INSC 184 2
|
| 546 |
+
51 1999 INSC 516 2
|
| 547 |
+
51 2010 INSC 392 2
|
| 548 |
+
52 2003 INSC 241 3
|
| 549 |
+
52 2015 INSC 92 2
|
| 550 |
+
52 2019 INSC 218 2
|
| 551 |
+
52 2019 INSC 647 2
|
| 552 |
+
52 2019 INSC 687 2
|
| 553 |
+
52 2020 INSC 705 2
|
| 554 |
+
52 2020 INSC 345 2
|
| 555 |
+
52 2021 INSC 269 2
|
| 556 |
+
52 2009 INSC 1278 2
|
| 557 |
+
52 2014 INSC 102 2
|
| 558 |
+
53 1953 INSC 89 3
|
| 559 |
+
53 2020 INSC 23 2
|
| 560 |
+
53 1954 INSC 90 2
|
| 561 |
+
53 1964 INSC 48 2
|
| 562 |
+
54 1950 INSC 14 3
|
| 563 |
+
54 2015 INSC 257 2
|
| 564 |
+
54 2018 INSC 880 2
|
| 565 |
+
54 2019 INSC 505 2
|
| 566 |
+
54 2019 INSC 517 2
|
| 567 |
+
54 2020 INSC 572 2
|
| 568 |
+
54 2023 INSC 4 2
|
| 569 |
+
54 1952 INSC 1 2
|
| 570 |
+
54 1985 INSC 238 2
|
| 571 |
+
54 2014 INSC 347 2
|
| 572 |
+
55 2008 INSC 473 3
|
| 573 |
+
55 2017 INSC 668 2
|
| 574 |
+
55 2018 INSC 881 2
|
| 575 |
+
55 2018 INSC 880 2
|
| 576 |
+
55 2019 INSC 680 2
|
| 577 |
+
55 2021 INSC 194 2
|
| 578 |
+
55 2022 INSC 1175 2
|
| 579 |
+
55 2023 INSC 145 2
|
| 580 |
+
55 2023 INSC 292 2
|
| 581 |
+
55 2023 INSC 559 2
|
| 582 |
+
55 2024 INSC 562 2
|
| 583 |
+
56 1954 INSC 125 3
|
| 584 |
+
56 2019 INSC 1146 2
|
| 585 |
+
56 2019 INSC 1406 2
|
| 586 |
+
56 2020 INSC 645 2
|
| 587 |
+
56 2020 INSC 620 2
|
| 588 |
+
56 2021 INSC 649 2
|
| 589 |
+
56 2022 INSC 318 2
|
| 590 |
+
56 2023 INSC 460 2
|
| 591 |
+
56 2024 INSC 363 2
|
| 592 |
+
56 1971 INSC 100 2
|
| 593 |
+
56 2009 INSC 611 2
|
| 594 |
+
57 2018 INSC 646 3
|
| 595 |
+
57 2018 INSC 1213 2
|
| 596 |
+
57 2019 INSC 1055 2
|
| 597 |
+
57 2019 INSC 937 2
|
| 598 |
+
57 2019 INSC 1067 2
|
| 599 |
+
57 2019 INSC 1257 2
|
| 600 |
+
57 2019 INSC 231 2
|
| 601 |
+
57 2019 INSC 410 2
|
| 602 |
+
57 2020 INSC 334 2
|
| 603 |
+
57 2020 INSC 415 2
|
| 604 |
+
57 2020 INSC 456 2
|
| 605 |
+
58 2012 INSC 419 3
|
| 606 |
+
58 2015 INSC 484 2
|
| 607 |
+
58 2017 INSC 683 2
|
| 608 |
+
58 2019 INSC 2 2
|
| 609 |
+
58 2019 INSC 254 2
|
| 610 |
+
58 2020 INSC 163 2
|
| 611 |
+
58 2021 INSC 650 2
|
| 612 |
+
58 2021 INSC 568 2
|
| 613 |
+
58 2022 INSC 940 2
|
| 614 |
+
58 2023 INSC 468 2
|
| 615 |
+
58 2024 INSC 846 2
|
| 616 |
+
59 1951 INSC 52 3
|
| 617 |
+
59 2020 INSC 635 2
|
| 618 |
+
59 2022 INSC 516 2
|
| 619 |
+
59 2023 INSC 1032 2
|
| 620 |
+
59 1999 INSC 407 2
|
| 621 |
+
59 2010 INSC 371 2
|
| 622 |
+
59 2011 INSC 113 2
|
| 623 |
+
59 2011 INSC 348 2
|
| 624 |
+
59 2011 INSC 366 2
|
| 625 |
+
59 2013 INSC 580 2
|
| 626 |
+
59 2013 INSC 94 2
|
| 627 |
+
60 2002 INSC 148 3
|
| 628 |
+
60 2017 INSC 1111 2
|
| 629 |
+
60 2019 INSC 1325 2
|
| 630 |
+
60 2020 INSC 665 2
|
| 631 |
+
60 2021 INSC 195 2
|
| 632 |
+
60 2021 INSC 919 2
|
| 633 |
+
60 2022 INSC 467 2
|
| 634 |
+
60 2022 INSC 57 2
|
| 635 |
+
60 2022 INSC 297 2
|
| 636 |
+
61 1996 INSC 952 3
|
| 637 |
+
61 2018 INSC 981 2
|
| 638 |
+
61 2018 INSC 804 2
|
| 639 |
+
61 2021 INSC 624 2
|
| 640 |
+
61 2024 INSC 178 2
|
| 641 |
+
61 2013 INSC 840 2
|
| 642 |
+
61 2013 INSC 834 2
|
| 643 |
+
62 1958 INSC 5 3
|
| 644 |
+
62 2015 INSC 485 2
|
| 645 |
+
62 2018 INSC 282 2
|
| 646 |
+
62 2024 INSC 812 2
|
| 647 |
+
62 2024 INSC 266 2
|
| 648 |
+
62 1962 INSC 348 2
|
| 649 |
+
62 1963 INSC 205 2
|
| 650 |
+
62 1977 INSC 211 2
|
| 651 |
+
62 1978 INSC 187 2
|
| 652 |
+
62 1979 INSC 244 2
|
| 653 |
+
62 2001 INSC 555 2
|
| 654 |
+
63 1960 INSC 211 3
|
| 655 |
+
63 2016 INSC 301 2
|
| 656 |
+
63 2021 INSC 659 2
|
| 657 |
+
63 2022 INSC 331 2
|
| 658 |
+
63 2023 INSC 81 2
|
| 659 |
+
63 2012 INSC 305 2
|
| 660 |
+
63 2018 INSC 244 2
|
| 661 |
+
64 2005 INSC 186 3
|
| 662 |
+
64 2017 INSC 896 2
|
| 663 |
+
64 2017 INSC 957 2
|
| 664 |
+
64 2018 INSC 648 2
|
| 665 |
+
64 2019 INSC 595 2
|
| 666 |
+
64 2020 INSC 274 2
|
| 667 |
+
64 2020 INSC 345 2
|
| 668 |
+
64 2021 INSC 754 2
|
| 669 |
+
64 2022 INSC 841 2
|
| 670 |
+
64 2024 INSC 627 2
|
| 671 |
+
64 2025 INSC 478 2
|
| 672 |
+
65 2002 INSC 165 3
|
| 673 |
+
65 2018 INSC 921 2
|
| 674 |
+
65 2018 INSC 288 2
|
| 675 |
+
65 2018 INSC 291 2
|
| 676 |
+
65 2019 INSC 1067 2
|
| 677 |
+
65 2019 INSC 1260 2
|
| 678 |
+
65 2020 INSC 376 2
|
| 679 |
+
65 2017 INSC 64 2
|
| 680 |
+
65 2022 INSC 304 2
|
| 681 |
+
65 2023 INSC 532 2
|
| 682 |
+
65 2023 INSC 564 2
|
| 683 |
+
66 2004 INSC 4 3
|
| 684 |
+
66 2018 INSC 781 2
|
| 685 |
+
66 2018 INSC 36 2
|
| 686 |
+
66 2018 INSC 206 2
|
| 687 |
+
66 2018 INSC 285 2
|
| 688 |
+
66 2018 INSC 192 2
|
| 689 |
+
66 2018 INSC 311 2
|
| 690 |
+
66 2018 INSC 531 2
|
| 691 |
+
66 2020 INSC 106 2
|
| 692 |
+
66 2018 INSC 700 2
|
| 693 |
+
66 2018 INSC 678 2
|
| 694 |
+
67 2006 INSC 452 3
|
| 695 |
+
67 2018 INSC 1060 2
|
| 696 |
+
67 2019 INSC 216 2
|
| 697 |
+
67 2019 INSC 663 2
|
| 698 |
+
67 2024 INSC 233 2
|
| 699 |
+
67 2008 INSC 955 2
|
| 700 |
+
67 2008 INSC 1438 2
|
| 701 |
+
67 2009 INSC 456 2
|
| 702 |
+
67 2010 INSC 532 2
|
| 703 |
+
67 2014 INSC 754 2
|
| 704 |
+
67 2022 INSC 1049 2
|
| 705 |
+
68 2012 INSC 68 3
|
| 706 |
+
68 2018 INSC 1018 2
|
| 707 |
+
68 2018 INSC 110 2
|
| 708 |
+
68 2018 INSC 455 2
|
| 709 |
+
68 2018 INSC 880 2
|
| 710 |
+
68 2022 INSC 255 2
|
| 711 |
+
68 2023 INSC 7 2
|
| 712 |
+
68 2014 INSC 962 2
|
| 713 |
+
68 2014 INSC 562 2
|
| 714 |
+
68 2019 INSC 1374 2
|
| 715 |
+
68 2023 INSC 459 2
|
| 716 |
+
69 2005 INSC 432 3
|
| 717 |
+
69 2016 INSC 298 2
|
| 718 |
+
69 2020 INSC 465 2
|
| 719 |
+
69 2021 INSC 798 2
|
| 720 |
+
69 2021 INSC 823 2
|
| 721 |
+
69 2022 INSC 1212 2
|
| 722 |
+
69 2022 INSC 775 2
|
| 723 |
+
69 2022 INSC 970 2
|
| 724 |
+
69 2025 INSC 1210 2
|
| 725 |
+
69 2025 INSC 223 2
|
| 726 |
+
69 2025 INSC 427 2
|
| 727 |
+
70 1952 INSC 10 3
|
| 728 |
+
70 2016 INSC 1019 2
|
| 729 |
+
70 1989 INSC 396 2
|
| 730 |
+
71 1998 INSC 400 3
|
| 731 |
+
71 2019 INSC 378 2
|
| 732 |
+
71 2025 INSC 25 2
|
| 733 |
+
71 2025 INSC 697 2
|
| 734 |
+
71 2005 INSC 418 2
|
| 735 |
+
71 2009 INSC 361 2
|
| 736 |
+
71 2011 INSC 772 2
|
| 737 |
+
72 2005 INSC 358 3
|
| 738 |
+
72 2020 INSC 557 2
|
| 739 |
+
72 2020 INSC 3 2
|
| 740 |
+
72 2022 INSC 1111 2
|
| 741 |
+
72 2022 INSC 1183 2
|
| 742 |
+
72 2008 INSC 585 2
|
| 743 |
+
72 2012 INSC 363 2
|
| 744 |
+
73 2004 INSC 34 3
|
| 745 |
+
73 2016 INSC 1019 2
|
| 746 |
+
73 2018 INSC 646 2
|
| 747 |
+
73 2020 INSC 382 2
|
| 748 |
+
73 2021 INSC 659 2
|
| 749 |
+
73 2022 INSC 331 2
|
| 750 |
+
73 2022 INSC 506 2
|
| 751 |
+
73 2023 INSC 27 2
|
| 752 |
+
73 2024 INSC 554 2
|
| 753 |
+
73 2011 INSC 336 2
|
| 754 |
+
74 2001 INSC 515 3
|
| 755 |
+
74 2019 INSC 810 2
|
| 756 |
+
74 2022 INSC 1073 2
|
| 757 |
+
74 2024 INSC 261 2
|
| 758 |
+
74 2024 INSC 519 2
|
| 759 |
+
74 2025 INSC 1089 2
|
| 760 |
+
74 2025 INSC 86 2
|
| 761 |
+
74 2025 INSC 168 2
|
| 762 |
+
74 2025 INSC 76 2
|
| 763 |
+
74 2012 INSC 376 2
|
| 764 |
+
74 2024 INSC 847 2
|
| 765 |
+
75 2006 INSC 532 3
|
| 766 |
+
75 2018 INSC 728 2
|
| 767 |
+
75 2018 INSC 880 2
|
| 768 |
+
75 2020 INSC 350 2
|
| 769 |
+
75 2023 INSC 856 2
|
| 770 |
+
76 1961 INSC 6 3
|
| 771 |
+
76 2016 INSC 943 2
|
| 772 |
+
76 2019 INSC 545 2
|
| 773 |
+
76 2020 INSC 63 2
|
| 774 |
+
76 2022 INSC 1173 2
|
| 775 |
+
76 2023 INSC 771 2
|
| 776 |
+
76 2023 INSC 990 2
|
| 777 |
+
76 2023 INSC 426 2
|
| 778 |
+
76 2010 INSC 159 2
|
| 779 |
+
76 2013 INSC 657 2
|
| 780 |
+
76 2013 INSC 101 2
|
| 781 |
+
77 1960 INSC 255 3
|
| 782 |
+
77 2021 INSC 227 2
|
| 783 |
+
77 1965 INSC 197 2
|
| 784 |
+
77 1974 INSC 220 2
|
| 785 |
+
77 1975 INSC 65 2
|
| 786 |
+
77 1989 INSC 304 2
|
| 787 |
+
77 1997 INSC 490 2
|
| 788 |
+
78 2002 INSC 189 3
|
| 789 |
+
78 2015 INSC 765 2
|
| 790 |
+
78 2015 INSC 485 2
|
| 791 |
+
78 2016 INSC 630 2
|
| 792 |
+
78 2017 INSC 1112 2
|
| 793 |
+
78 2019 INSC 1248 2
|
| 794 |
+
78 2019 INSC 1242 2
|
| 795 |
+
78 2022 INSC 1056 2
|
| 796 |
+
78 2024 INSC 145 2
|
| 797 |
+
78 2025 INSC 1308 2
|
| 798 |
+
78 2006 INSC 117 2
|
| 799 |
+
79 2000 INSC 34 3
|
| 800 |
+
79 2018 INSC 1060 2
|
| 801 |
+
79 2021 INSC 132 2
|
| 802 |
+
79 2021 INSC 675 2
|
| 803 |
+
79 2022 INSC 326 2
|
| 804 |
+
79 2022 INSC 825 2
|
| 805 |
+
79 2008 INSC 1460 2
|
| 806 |
+
80 2002 INSC 136 3
|
| 807 |
+
80 2015 INSC 647 2
|
| 808 |
+
80 2017 INSC 658 2
|
| 809 |
+
80 2018 INSC 200 2
|
| 810 |
+
80 2018 INSC 115 2
|
| 811 |
+
80 2020 INSC 294 2
|
| 812 |
+
80 2020 INSC 320 2
|
| 813 |
+
80 2021 INSC 332 2
|
| 814 |
+
80 2021 INSC 554 2
|
| 815 |
+
80 2007 INSC 599 2
|
| 816 |
+
80 2011 INSC 843 2
|
| 817 |
+
81 2014 INSC 358 3
|
| 818 |
+
81 2015 INSC 912 2
|
| 819 |
+
81 2016 INSC 955 2
|
| 820 |
+
81 2017 INSC 1030 2
|
| 821 |
+
81 2017 INSC 143 2
|
| 822 |
+
81 2019 INSC 860 2
|
| 823 |
+
81 2019 INSC 889 2
|
| 824 |
+
81 2019 INSC 1010 2
|
| 825 |
+
81 2023 INSC 292 2
|
| 826 |
+
81 2024 INSC 1003 2
|
| 827 |
+
81 2024 INSC 41 2
|
| 828 |
+
82 2008 INSC 785 3
|
| 829 |
+
82 2018 INSC 78 2
|
| 830 |
+
82 2018 INSC 248 2
|
| 831 |
+
82 2018 INSC 714 2
|
| 832 |
+
82 2020 INSC 620 2
|
| 833 |
+
82 2020 INSC 524 2
|
| 834 |
+
82 2022 INSC 164 2
|
| 835 |
+
82 2023 INSC 634 2
|
| 836 |
+
82 2024 INSC 511 2
|
| 837 |
+
82 2010 INSC 495 2
|
| 838 |
+
82 2011 INSC 109 2
|
| 839 |
+
83 2003 INSC 391 3
|
| 840 |
+
83 2016 INSC 332 2
|
| 841 |
+
83 2004 INSC 234 2
|
| 842 |
+
83 2011 INSC 388 2
|
| 843 |
+
83 2012 INSC 428 2
|
| 844 |
+
84 2017 INSC 1026 3
|
| 845 |
+
84 2018 INSC 732 2
|
| 846 |
+
84 2019 INSC 1008 2
|
| 847 |
+
84 2019 INSC 1292 2
|
| 848 |
+
84 2019 INSC 415 2
|
| 849 |
+
84 2019 INSC 511 2
|
| 850 |
+
84 2020 INSC 697 2
|
| 851 |
+
84 2020 INSC 380 2
|
| 852 |
+
84 2021 INSC 175 2
|
| 853 |
+
84 2021 INSC 229 2
|
| 854 |
+
84 2021 INSC 12 2
|
| 855 |
+
85 1995 INSC 661 3
|
| 856 |
+
85 2019 INSC 822 2
|
| 857 |
+
85 2022 INSC 451 2
|
| 858 |
+
85 2022 INSC 322 2
|
| 859 |
+
85 2022 INSC 436 2
|
| 860 |
+
85 2025 INSC 264 2
|
| 861 |
+
85 1999 INSC 499 2
|
| 862 |
+
85 2007 INSC 805 2
|
| 863 |
+
85 2002 INSC 234 2
|
| 864 |
+
85 2006 INSC 49 2
|
| 865 |
+
86 2018 INSC 790 3
|
| 866 |
+
86 2019 INSC 518 2
|
| 867 |
+
86 2021 INSC 28 2
|
| 868 |
+
86 2021 INSC 303 2
|
| 869 |
+
86 2021 INSC 777 2
|
| 870 |
+
86 2022 INSC 411 2
|
| 871 |
+
86 2022 INSC 1085 2
|
| 872 |
+
86 2023 INSC 920 2
|
| 873 |
+
86 2023 INSC 115 2
|
| 874 |
+
86 2023 INSC 144 2
|
| 875 |
+
86 2023 INSC 99 2
|
| 876 |
+
87 2014 INSC 568 3
|
| 877 |
+
87 2015 INSC 726 2
|
| 878 |
+
87 2015 INSC 912 2
|
| 879 |
+
87 2016 INSC 526 2
|
| 880 |
+
87 2017 INSC 314 2
|
| 881 |
+
87 2017 INSC 892 2
|
| 882 |
+
87 2018 INSC 862 2
|
| 883 |
+
87 2018 INSC 728 2
|
| 884 |
+
87 2019 INSC 194 2
|
| 885 |
+
87 2018 INSC 790 2
|
| 886 |
+
87 2019 INSC 1007 2
|
| 887 |
+
88 1957 INSC 99 3
|
| 888 |
+
88 2019 INSC 1231 2
|
| 889 |
+
88 2021 INSC 284 2
|
| 890 |
+
88 2022 INSC 1085 2
|
| 891 |
+
88 2024 INSC 119 2
|
| 892 |
+
88 1961 INSC 350 2
|
| 893 |
+
88 1962 INSC 1 2
|
| 894 |
+
88 1990 INSC 168 2
|
| 895 |
+
88 1997 INSC 268 2
|
| 896 |
+
88 2014 INSC 362 2
|
| 897 |
+
89 1963 INSC 172 3
|
| 898 |
+
89 2016 INSC 301 2
|
| 899 |
+
89 2020 INSC 382 2
|
| 900 |
+
89 2020 INSC 346 2
|
| 901 |
+
89 2022 INSC 331 2
|
| 902 |
+
89 1990 INSC 232 2
|
| 903 |
+
89 1991 INSC 90 2
|
| 904 |
+
89 1996 INSC 222 2
|
| 905 |
+
89 2002 INSC 44 2
|
| 906 |
+
89 2012 INSC 305 2
|
| 907 |
+
89 2014 INSC 331 2
|
| 908 |
+
90 1994 INSC 6 3
|
| 909 |
+
90 2017 INSC 448 2
|
| 910 |
+
90 2019 INSC 1114 2
|
| 911 |
+
90 2019 INSC 851 2
|
| 912 |
+
90 2020 INSC 624 2
|
| 913 |
+
90 2022 INSC 164 2
|
| 914 |
+
90 2005 INSC 282 2
|
| 915 |
+
90 2008 INSC 997 2
|
| 916 |
+
90 2012 INSC 565 2
|
| 917 |
+
90 2013 INSC 281 2
|
| 918 |
+
91 2003 INSC 258 3
|
| 919 |
+
91 2019 INSC 1020 2
|
| 920 |
+
91 2019 INSC 1048 2
|
| 921 |
+
91 2021 INSC 326 2
|
| 922 |
+
91 2023 INSC 96 2
|
| 923 |
+
91 2007 INSC 822 2
|
| 924 |
+
91 2008 INSC 334 2
|
| 925 |
+
91 2008 INSC 517 2
|
| 926 |
+
91 2011 INSC 651 2
|
| 927 |
+
91 2014 INSC 374 2
|
| 928 |
+
92 2001 INSC 251 3
|
| 929 |
+
92 2017 INSC 1111 2
|
| 930 |
+
92 2020 INSC 706 2
|
| 931 |
+
92 2021 INSC 50 2
|
| 932 |
+
92 2022 INSC 431 2
|
| 933 |
+
92 2022 INSC 427 2
|
| 934 |
+
92 2008 INSC 1201 2
|
| 935 |
+
92 2009 INSC 140 2
|
| 936 |
+
92 2012 INSC 477 2
|
| 937 |
+
93 2011 INSC 554 3
|
| 938 |
+
93 2018 INSC 880 2
|
| 939 |
+
93 2020 INSC 492 2
|
| 940 |
+
93 2020 INSC 653 2
|
| 941 |
+
93 2020 INSC 482 2
|
| 942 |
+
93 2020 INSC 688 2
|
| 943 |
+
93 2020 INSC 23 2
|
| 944 |
+
93 2021 INSC 115 2
|
| 945 |
+
93 2022 INSC 430 2
|
| 946 |
+
93 2023 INSC 123 2
|
| 947 |
+
93 2023 INSC 81 2
|
| 948 |
+
94 1950 INSC 36 3
|
| 949 |
+
94 2018 INSC 790 2
|
| 950 |
+
94 2022 INSC 378 2
|
| 951 |
+
94 2011 INSC 304 2
|
| 952 |
+
94 2012 INSC 45 2
|
| 953 |
+
94 2012 INSC 234 2
|
| 954 |
+
94 2013 INSC 510 2
|
| 955 |
+
94 1959 INSC 2 2
|
| 956 |
+
95 2001 INSC 487 3
|
| 957 |
+
95 2016 INSC 1187 2
|
| 958 |
+
95 2018 INSC 896 2
|
| 959 |
+
95 2018 INSC 1158 2
|
| 960 |
+
95 2019 INSC 1112 2
|
| 961 |
+
95 2021 INSC 199 2
|
| 962 |
+
95 2024 INSC 944 2
|
| 963 |
+
95 2010 INSC 212 2
|
| 964 |
+
95 2014 INSC 525 2
|
| 965 |
+
96 2002 INSC 433 3
|
| 966 |
+
96 2021 INSC 698 2
|
| 967 |
+
96 2025 INSC 880 2
|
| 968 |
+
96 2008 INSC 952 2
|
| 969 |
+
96 2008 INSC 512 2
|
| 970 |
+
96 2011 INSC 645 2
|
| 971 |
+
96 2012 INSC 565 2
|
| 972 |
+
96 2012 INSC 243 2
|
| 973 |
+
96 2013 INSC 67 2
|
| 974 |
+
96 2014 INSC 847 2
|
| 975 |
+
97 2012 INSC 379 3
|
| 976 |
+
97 2015 INSC 201 2
|
| 977 |
+
97 2017 INSC 369 2
|
| 978 |
+
97 2018 INSC 724 2
|
| 979 |
+
97 2019 INSC 1349 2
|
| 980 |
+
97 2019 INSC 817 2
|
| 981 |
+
97 2020 INSC 659 2
|
| 982 |
+
97 2020 INSC 294 2
|
| 983 |
+
97 2020 INSC 284 2
|
| 984 |
+
97 2021 INSC 264 2
|
| 985 |
+
97 2022 INSC 1299 2
|
| 986 |
+
98 1999 INSC 235 3
|
| 987 |
+
98 2015 INSC 436 2
|
| 988 |
+
98 2017 INSC 448 2
|
| 989 |
+
98 2018 INSC 658 2
|
| 990 |
+
98 2020 INSC 325 2
|
| 991 |
+
98 2024 INSC 1007 2
|
| 992 |
+
98 2025 INSC 1090 2
|
| 993 |
+
98 2000 INSC 308 2
|
| 994 |
+
98 2008 INSC 534 2
|
| 995 |
+
98 2009 INSC 1019 2
|
| 996 |
+
98 2010 INSC 499 2
|
| 997 |
+
99 1996 INSC 612 3
|
| 998 |
+
99 2008 INSC 1024 2
|
| 999 |
+
99 2008 INSC 476 2
|
| 1000 |
+
99 2008 INSC 646 2
|
| 1001 |
+
99 2014 INSC 147 2
|
| 1002 |
+
100 2005 INSC 58 3
|
| 1003 |
+
100 2020 INSC 577 2
|
| 1004 |
+
100 2020 INSC 508 2
|
| 1005 |
+
100 2010 INSC 238 2
|
| 1006 |
+
100 2012 INSC 395 2
|
| 1007 |
+
100 2013 INSC 176 2
|
| 1008 |
+
100 2013 INSC 283 2
|
| 1009 |
+
100 2014 INSC 221 2
|
| 1010 |
+
100 1960 INSC 88 2
|
| 1011 |
+
100 1969 INSC 266 2
|
| 1012 |
+
100 1971 INSC 322 2
|
| 1013 |
+
101 2005 INSC 334 3
|
| 1014 |
+
101 2017 INSC 826 2
|
| 1015 |
+
101 2017 INSC 450 2
|
| 1016 |
+
101 2018 INSC 915 2
|
| 1017 |
+
101 2019 INSC 1378 2
|
| 1018 |
+
101 2019 INSC 1242 2
|
| 1019 |
+
101 2019 INSC 266 2
|
| 1020 |
+
101 2021 INSC 304 2
|
| 1021 |
+
101 2008 INSC 785 2
|
| 1022 |
+
101 2009 INSC 657 2
|
| 1023 |
+
101 2009 INSC 811 2
|
| 1024 |
+
102 2002 INSC 39 3
|
| 1025 |
+
102 2019 INSC 247 2
|
| 1026 |
+
102 2019 INSC 1114 2
|
| 1027 |
+
102 2019 INSC 196 2
|
| 1028 |
+
102 2022 INSC 606 2
|
| 1029 |
+
102 2007 INSC 901 2
|
| 1030 |
+
102 2008 INSC 880 2
|
| 1031 |
+
102 2008 INSC 512 2
|
| 1032 |
+
103 1960 INSC 15 3
|
| 1033 |
+
103 1975 INSC 240 2
|
| 1034 |
+
103 1978 INSC 41 2
|
| 1035 |
+
103 2000 INSC 459 2
|
| 1036 |
+
103 2009 INSC 1035 2
|
| 1037 |
+
103 2009 INSC 1037 2
|
| 1038 |
+
104 1963 INSC 173 3
|
| 1039 |
+
104 2015 INSC 647 2
|
| 1040 |
+
104 2019 INSC 895 2
|
| 1041 |
+
104 2019 INSC 215 2
|
| 1042 |
+
104 2021 INSC 703 2
|
| 1043 |
+
104 2022 INSC 779 2
|
| 1044 |
+
104 2011 INSC 814 2
|
| 1045 |
+
104 2011 INSC 842 2
|
| 1046 |
+
104 2012 INSC 122 2
|
| 1047 |
+
104 2013 INSC 663 2
|
| 1048 |
+
104 2014 INSC 494 2
|
| 1049 |
+
105 1964 INSC 17 3
|
| 1050 |
+
105 2020 INSC 344 2
|
| 1051 |
+
105 2023 INSC 81 2
|
| 1052 |
+
105 1980 INSC 218 2
|
| 1053 |
+
105 S_1992_2_454_1007 2
|
| 1054 |
+
105 2011 INSC 516 2
|
| 1055 |
+
105 2011 INSC 246 2
|
| 1056 |
+
105 2013 INSC 41 2
|
| 1057 |
+
106 2012 INSC 187 3
|
| 1058 |
+
106 2017 INSC 830 2
|
| 1059 |
+
106 2019 INSC 1283 2
|
| 1060 |
+
106 2019 INSC 53 2
|
| 1061 |
+
106 2020 INSC 634 2
|
| 1062 |
+
106 2021 INSC 699 2
|
| 1063 |
+
106 2021 INSC 133 2
|
| 1064 |
+
106 2022 INSC 1212 2
|
| 1065 |
+
106 2022 INSC 578 2
|
| 1066 |
+
106 2023 INSC 956 2
|
| 1067 |
+
106 2024 INSC 551 2
|
| 1068 |
+
107 2005 INSC 433 3
|
| 1069 |
+
107 2022 INSC 394 2
|
| 1070 |
+
107 2011 INSC 843 2
|
| 1071 |
+
107 2011 INSC 590 2
|
| 1072 |
+
107 2013 INSC 199 2
|
| 1073 |
+
107 2014 INSC 68 2
|
| 1074 |
+
107 2013 INSC 133 2
|
| 1075 |
+
108 2011 INSC 379 3
|
| 1076 |
+
108 2018 INSC 797 2
|
| 1077 |
+
108 2018 INSC 115 2
|
| 1078 |
+
108 2018 INSC 159 2
|
| 1079 |
+
108 2020 INSC 376 2
|
| 1080 |
+
108 2020 INSC 294 2
|
| 1081 |
+
108 2022 INSC 401 2
|
| 1082 |
+
108 2022 INSC 767 2
|
| 1083 |
+
108 2022 INSC 560 2
|
| 1084 |
+
108 2011 INSC 531 2
|
| 1085 |
+
108 2012 INSC 65 2
|
| 1086 |
+
109 2008 INSC 1234 3
|
| 1087 |
+
109 2016 INSC 1111 2
|
| 1088 |
+
109 2016 INSC 993 2
|
| 1089 |
+
109 2021 INSC 209 2
|
| 1090 |
+
109 2022 INSC 452 2
|
| 1091 |
+
109 2023 INSC 460 2
|
| 1092 |
+
109 2024 INSC 466 2
|
| 1093 |
+
109 2009 INSC 1010 2
|
| 1094 |
+
109 2009 INSC 1018 2
|
| 1095 |
+
109 2009 INSC 162 2
|
| 1096 |
+
109 2009 INSC 440 2
|
| 1097 |
+
110 1957 INSC 79 3
|
| 1098 |
+
110 2016 INSC 1049 2
|
| 1099 |
+
110 2019 INSC 871 2
|
| 1100 |
+
110 2022 INSC 1139 2
|
| 1101 |
+
110 2022 INSC 133 2
|
| 1102 |
+
110 2023 INSC 978 2
|
| 1103 |
+
110 2023 INSC 924 2
|
| 1104 |
+
110 1976 INSC 140 2
|
| 1105 |
+
110 1983 INSC 3 2
|
| 1106 |
+
110 1985 INSC 11 2
|
| 1107 |
+
110 2000 INSC 375 2
|
| 1108 |
+
111 1991 INSC 225 3
|
| 1109 |
+
111 2017 INSC 462 2
|
| 1110 |
+
111 2018 INSC 912 2
|
| 1111 |
+
111 2018 INSC 369 2
|
| 1112 |
+
111 2019 INSC 1007 2
|
| 1113 |
+
111 2020 INSC 373 2
|
| 1114 |
+
111 2024_11_1647_2038 2
|
| 1115 |
+
111 2023 INSC 190 2
|
| 1116 |
+
111 2025 INSC 694 2
|
| 1117 |
+
111 1995 INSC 179 2
|
| 1118 |
+
111 1998 INSC 382 2
|
| 1119 |
+
112 1994 INSC 371 3
|
| 1120 |
+
112 2017 INSC 754 2
|
| 1121 |
+
112 2020 INSC 589 2
|
| 1122 |
+
112 2022 INSC 1013 2
|
| 1123 |
+
112 2024 INSC 58 2
|
| 1124 |
+
112 2011 INSC 734 2
|
| 1125 |
+
112 2011 INSC 706 2
|
| 1126 |
+
112 2013 INSC 97 2
|
| 1127 |
+
113 2015 INSC 886 3
|
| 1128 |
+
113 2019 INSC 1007 2
|
| 1129 |
+
113 2019 INSC 1116 2
|
| 1130 |
+
113 2019 INSC 1107 2
|
| 1131 |
+
113 2019 INSC 574 2
|
| 1132 |
+
113 2019 INSC 545 2
|
| 1133 |
+
113 2019 INSC 518 2
|
| 1134 |
+
113 2021 INSC 223 2
|
| 1135 |
+
113 2022 INSC 164 2
|
| 1136 |
+
113 2022 INSC 565 2
|
| 1137 |
+
113 2022 INSC 939 2
|
| 1138 |
+
114 2006 INSC 691 3
|
| 1139 |
+
114 2016 INSC 464 2
|
| 1140 |
+
114 2018 INSC 985 2
|
| 1141 |
+
114 2019 INSC 420 2
|
| 1142 |
+
114 2020 INSC 649 2
|
| 1143 |
+
114 2021 INSC 136 2
|
| 1144 |
+
114 2022 INSC 1079 2
|
| 1145 |
+
114 2022 INSC 608 2
|
| 1146 |
+
114 2024 INSC 809 2
|
| 1147 |
+
114 2024 INSC 19 2
|
| 1148 |
+
114 2024 INSC 211 2
|
| 1149 |
+
115 2001 INSC 323 3
|
| 1150 |
+
115 2018 INSC 115 2
|
| 1151 |
+
115 2020 INSC 294 2
|
| 1152 |
+
115 2021 INSC 817 2
|
| 1153 |
+
115 2022 INSC 840 2
|
| 1154 |
+
115 2023 INSC 373 2
|
| 1155 |
+
115 2011 INSC 638 2
|
| 1156 |
+
116 1995 INSC 100 3
|
| 1157 |
+
116 2016_9_771_799 2
|
| 1158 |
+
116 2019 INSC 1099 2
|
| 1159 |
+
116 2019 INSC 1120 2
|
| 1160 |
+
116 2019 INSC 1387 2
|
| 1161 |
+
116 2021 INSC 731 2
|
| 1162 |
+
116 2022 INSC 647 2
|
| 1163 |
+
116 2006 INSC 352 2
|
| 1164 |
+
116 2008 INSC 304 2
|
| 1165 |
+
116 2009 INSC 254 2
|
| 1166 |
+
116 2012 INSC 54 2
|
| 1167 |
+
117 1960 INSC 163 3
|
| 1168 |
+
117 2016 INSC 1019 2
|
| 1169 |
+
117 2020 INSC 408 2
|
| 1170 |
+
117 2022 INSC 331 2
|
| 1171 |
+
117 2023 INSC 81 2
|
| 1172 |
+
117 2025 INSC 1154 2
|
| 1173 |
+
117 1997 INSC 305 2
|
| 1174 |
+
117 1999 INSC 230 2
|
| 1175 |
+
117 2004 INSC 352 2
|
| 1176 |
+
117 2013 INSC 707 2
|
| 1177 |
+
117 2007 INSC 22 2
|
| 1178 |
+
118 2007 INSC 241 3
|
| 1179 |
+
118 2017 INSC 467 2
|
| 1180 |
+
118 2020 INSC 563 2
|
| 1181 |
+
118 2021 INSC 145 2
|
| 1182 |
+
118 2021 INSC 159 2
|
| 1183 |
+
118 2021 INSC 285 2
|
| 1184 |
+
118 2007 INSC 1267 2
|
| 1185 |
+
118 2008 INSC 877 2
|
| 1186 |
+
118 2008 INSC 476 2
|
| 1187 |
+
118 2008 INSC 646 2
|
| 1188 |
+
118 2009 INSC 482 2
|
| 1189 |
+
119 2019 INSC 647 3
|
| 1190 |
+
119 2020 INSC 548 2
|
| 1191 |
+
119 2020 INSC 705 2
|
| 1192 |
+
119 2020 INSC 403 2
|
| 1193 |
+
119 2021 INSC 712 2
|
| 1194 |
+
119 2021 INSC 140 2
|
| 1195 |
+
119 2021 INSC 365 2
|
| 1196 |
+
119 2021 INSC 344 2
|
| 1197 |
+
119 2021 INSC 464 2
|
| 1198 |
+
119 2021 INSC 392 2
|
| 1199 |
+
119 2022 INSC 483 2
|
| 1200 |
+
120 2015 INSC 163 3
|
| 1201 |
+
120 2015 INSC 823 2
|
| 1202 |
+
120 2016 INSC 355 2
|
| 1203 |
+
120 2017 INSC 1042 2
|
| 1204 |
+
120 2017 INSC 1014 2
|
| 1205 |
+
120 2018 INSC 711 2
|
| 1206 |
+
120 2019 INSC 593 2
|
| 1207 |
+
120 2022 INSC 853 2
|
| 1208 |
+
120 2022 INSC 669 2
|
| 1209 |
+
120 2023 INSC 1030 2
|
| 1210 |
+
120 2024 INSC 900 2
|
| 1211 |
+
121 2000 INSC 405 3
|
| 1212 |
+
121 2015 INSC 396 2
|
| 1213 |
+
121 2017 INSC 293 2
|
| 1214 |
+
121 2020 INSC 321 2
|
| 1215 |
+
121 2021 INSC 599 2
|
| 1216 |
+
121 2021 INSC 45 2
|
| 1217 |
+
121 2023 INSC 87 2
|
| 1218 |
+
121 2024 INSC 213 2
|
| 1219 |
+
121 2004 INSC 373 2
|
| 1220 |
+
121 2013 INSC 6 2
|
| 1221 |
+
122 1960 INSC 256 3
|
| 1222 |
+
122 2021 INSC 200 2
|
| 1223 |
+
122 2022 INSC 19 2
|
| 1224 |
+
122 2024 INSC 104 2
|
| 1225 |
+
122 1963 INSC 151 2
|
| 1226 |
+
122 1997 INSC 609 2
|
| 1227 |
+
122 2011 INSC 565 2
|
| 1228 |
+
122 2013 INSC 225 2
|
| 1229 |
+
122 2008 INSC 888 2
|
| 1230 |
+
122 2008 INSC 887 2
|
| 1231 |
+
122 2008 INSC 1024 2
|
| 1232 |
+
123 1996 INSC 75 3
|
| 1233 |
+
123 2017 INSC 448 2
|
| 1234 |
+
123 2018 INSC 1192 2
|
| 1235 |
+
123 2018 INSC 1120 2
|
| 1236 |
+
123 2020 INSC 192 2
|
| 1237 |
+
123 2021 INSC 192 2
|
| 1238 |
+
123 2021 INSC 298 2
|
| 1239 |
+
123 2022 INSC 648 2
|
| 1240 |
+
123 2023 INSC 959 2
|
| 1241 |
+
123 2008 INSC 830 2
|
| 1242 |
+
123 2008 INSC 1474 2
|
| 1243 |
+
124 2005 INSC 190 3
|
| 1244 |
+
124 2019 INSC 149 2
|
| 1245 |
+
124 2019 INSC 456 2
|
| 1246 |
+
124 2021 INSC 654 2
|
| 1247 |
+
124 2021 INSC 688 2
|
| 1248 |
+
124 2022 INSC 1288 2
|
| 1249 |
+
124 2006 INSC 856 2
|
| 1250 |
+
124 2008 INSC 44 2
|
| 1251 |
+
124 2012 INSC 494 2
|
| 1252 |
+
124 2006 INSC 342 2
|
| 1253 |
+
124 2007 INSC 839 2
|
| 1254 |
+
125 1959 INSC 2 3
|
| 1255 |
+
125 2016_8_477_498 2
|
| 1256 |
+
125 2018 INSC 880 2
|
| 1257 |
+
125 2020 INSC 285 2
|
| 1258 |
+
125 2020 INSC 451 2
|
| 1259 |
+
125 2022 INSC 134 2
|
| 1260 |
+
125 2022 INSC 545 2
|
| 1261 |
+
125 2024 INSC 812 2
|
| 1262 |
+
125 1985 INSC 121 2
|
| 1263 |
+
125 1990 INSC 59 2
|
| 1264 |
+
125 2021 INSC 798 2
|
| 1265 |
+
126 2014 INSC 841 3
|
| 1266 |
+
126 2016 INSC 1061 2
|
| 1267 |
+
126 2017 INSC 283 2
|
| 1268 |
+
126 2019 INSC 1325 2
|
| 1269 |
+
126 2021 INSC 271 2
|
| 1270 |
+
126 2021 INSC 343 2
|
| 1271 |
+
126 2022 INSC 431 2
|
| 1272 |
+
126 2022 INSC 427 2
|
| 1273 |
+
126 2023 INSC 252 2
|
| 1274 |
+
126 2025 INSC 979 2
|
| 1275 |
+
126 2025 INSC 935 2
|
| 1276 |
+
127 2019 INSC 889 3
|
| 1277 |
+
127 2019 INSC 1289 2
|
| 1278 |
+
127 2020 INSC 701 2
|
| 1279 |
+
127 2020 INSC 625 2
|
| 1280 |
+
127 2020 INSC 227 2
|
| 1281 |
+
127 2021 INSC 133 2
|
| 1282 |
+
127 2021 INSC 59 2
|
| 1283 |
+
127 2021 INSC 51 2
|
| 1284 |
+
127 2021 INSC 28 2
|
| 1285 |
+
127 2021 INSC 206 2
|
| 1286 |
+
127 2021 INSC 233 2
|
| 1287 |
+
128 1962 INSC 279 3
|
| 1288 |
+
128 2020 INSC 344 2
|
| 1289 |
+
128 2022 INSC 73 2
|
| 1290 |
+
128 2022 INSC 1175 2
|
| 1291 |
+
128 2024 INSC 562 2
|
| 1292 |
+
128 1995 INSC 375 2
|
| 1293 |
+
128 2011 INSC 388 2
|
| 1294 |
+
129 2019 INSC 1256 3
|
| 1295 |
+
129 2021 INSC 468 2
|
| 1296 |
+
129 2021 INSC 206 2
|
| 1297 |
+
129 2021 INSC 187 2
|
| 1298 |
+
129 2021 INSC 227 2
|
| 1299 |
+
129 2021 INSC 296 2
|
| 1300 |
+
129 2021 INSC 395 2
|
| 1301 |
+
129 2021 INSC 923 2
|
| 1302 |
+
129 2022 INSC 957 2
|
| 1303 |
+
129 2023 INSC 625 2
|
| 1304 |
+
129 2023 INSC 232 2
|
| 1305 |
+
130 2002 INSC 138 3
|
| 1306 |
+
130 2015 INSC 201 2
|
| 1307 |
+
130 2016 INSC 1196 2
|
| 1308 |
+
130 2017 INSC 589 2
|
| 1309 |
+
130 2019 INSC 817 2
|
| 1310 |
+
130 2020 INSC 659 2
|
| 1311 |
+
130 2021 INSC 264 2
|
| 1312 |
+
130 2024 INSC 857 2
|
| 1313 |
+
130 2024 INSC 607 2
|
| 1314 |
+
130 2007 INSC 796 2
|
| 1315 |
+
130 2011 INSC 683 2
|
| 1316 |
+
131 1999 INSC 407 3
|
| 1317 |
+
131 2020 INSC 339 2
|
| 1318 |
+
131 2020 INSC 364 2
|
| 1319 |
+
131 2000 INSC 562 2
|
| 1320 |
+
131 2006 INSC 711 2
|
| 1321 |
+
131 2012 INSC 558 2
|
| 1322 |
+
132 2014 INSC 590 3
|
| 1323 |
+
132 2017 INSC 801 2
|
| 1324 |
+
132 2018 INSC 1031 2
|
| 1325 |
+
132 2018 INSC 1193 2
|
| 1326 |
+
132 2019 INSC 1107 2
|
| 1327 |
+
132 2019 INSC 1216 2
|
| 1328 |
+
132 2019 INSC 518 2
|
| 1329 |
+
132 2019 INSC 196 2
|
| 1330 |
+
132 2020 INSC 624 2
|
| 1331 |
+
132 2022 INSC 565 2
|
| 1332 |
+
132 2023 INSC 264 2
|
| 1333 |
+
133 2009 INSC 693 3
|
| 1334 |
+
133 2015 INSC 1044 2
|
| 1335 |
+
133 2016 INSC 1184 2
|
| 1336 |
+
133 2020 INSC 577 2
|
| 1337 |
+
133 2021 INSC 118 2
|
| 1338 |
+
133 2010 INSC 238 2
|
| 1339 |
+
133 2011 INSC 20 2
|
| 1340 |
+
133 2011 INSC 552 2
|
| 1341 |
+
133 2013 INSC 176 2
|
| 1342 |
+
133 1997 INSC 275 2
|
| 1343 |
+
133 2012 INSC 70 2
|
| 1344 |
+
134 2013 INSC 179 3
|
| 1345 |
+
134 2016 INSC 934 2
|
| 1346 |
+
134 2020 INSC 276 2
|
| 1347 |
+
134 2021 INSC 218 2
|
| 1348 |
+
134 2021 INSC 283 2
|
| 1349 |
+
134 2022 INSC 1252 2
|
| 1350 |
+
134 2023 INSC 971 2
|
| 1351 |
+
134 2023 INSC 998 2
|
| 1352 |
+
135 1954 INSC 5 3
|
| 1353 |
+
135 2021 INSC 374 2
|
| 1354 |
+
135 2023 INSC 499 2
|
| 1355 |
+
135 1955 INSC 35 2
|
| 1356 |
+
135 1984 INSC 203 2
|
| 1357 |
+
135 2008 INSC 1337 2
|
| 1358 |
+
135 2009 INSC 1160 2
|
| 1359 |
+
135 1999 INSC 271 2
|
| 1360 |
+
136 2010 INSC 146 3
|
| 1361 |
+
136 2018 INSC 629 2
|
| 1362 |
+
136 2019 INSC 1300 2
|
| 1363 |
+
136 2019 INSC 63 2
|
| 1364 |
+
136 2019 INSC 718 2
|
| 1365 |
+
136 2020 INSC 557 2
|
| 1366 |
+
136 2020 INSC 452 2
|
| 1367 |
+
136 2020 INSC 218 2
|
| 1368 |
+
136 2021 INSC 558 2
|
| 1369 |
+
136 2024 INSC 791 2
|
| 1370 |
+
136 2023 INSC 560 2
|
| 1371 |
+
137 1952 INSC 28 3
|
| 1372 |
+
137 2017 INSC 945 2
|
| 1373 |
+
137 2019 INSC 597 2
|
| 1374 |
+
137 2025 INSC 481 2
|
| 1375 |
+
137 2008 INSC 1017 2
|
| 1376 |
+
137 2010 INSC 589 2
|
| 1377 |
+
137 2012 INSC 564 2
|
| 1378 |
+
137 2012 INSC 305 2
|
| 1379 |
+
137 2014 INSC 864 2
|
| 1380 |
+
138 2014 INSC 463 3
|
| 1381 |
+
138 2016 INSC 441 2
|
| 1382 |
+
138 2018 INSC 820 2
|
| 1383 |
+
138 2018 INSC 248 2
|
| 1384 |
+
138 2020 INSC 517 2
|
| 1385 |
+
138 2021 INSC 295 2
|
| 1386 |
+
138 2022 INSC 736 2
|
| 1387 |
+
138 2022 INSC 163 2
|
| 1388 |
+
138 2023 INSC 660 2
|
| 1389 |
+
138 2023 INSC 677 2
|
| 1390 |
+
138 2023 INSC 380 2
|
| 1391 |
+
139 2004 INSC 585 3
|
| 1392 |
+
139 2017 INSC 1286 2
|
| 1393 |
+
139 2018 INSC 797 2
|
| 1394 |
+
139 2019 INSC 400 2
|
| 1395 |
+
139 2020 INSC 525 2
|
| 1396 |
+
139 2021 INSC 817 2
|
| 1397 |
+
139 2022 INSC 757 2
|
| 1398 |
+
139 2008 INSC 938 2
|
| 1399 |
+
139 2009 INSC 885 2
|
| 1400 |
+
139 2020 INSC 294 2
|
| 1401 |
+
140 1999 INSC 299 3
|
| 1402 |
+
140 2020 INSC 697 2
|
| 1403 |
+
140 2023 INSC 4 2
|
| 1404 |
+
140 2000 INSC 38 2
|
| 1405 |
+
140 2003 INSC 638 2
|
| 1406 |
+
140 2010 INSC 624 2
|
| 1407 |
+
141 1955 INSC 15 3
|
| 1408 |
+
141 2017 INSC 855 2
|
| 1409 |
+
141 2018 INSC 1140 2
|
| 1410 |
+
141 2018 INSC 221 2
|
| 1411 |
+
141 2019 INSC 823 2
|
| 1412 |
+
141 2019 INSC 1236 2
|
| 1413 |
+
141 2020 INSC 320 2
|
| 1414 |
+
141 2022 INSC 579 2
|
| 1415 |
+
141 2022 INSC 681 2
|
| 1416 |
+
141 2023 INSC 717 2
|
| 1417 |
+
141 2004 INSC 608 2
|
| 1418 |
+
142 2006 INSC 326 3
|
| 1419 |
+
142 2017 INSC 1281 2
|
| 1420 |
+
142 2018 INSC 53 2
|
| 1421 |
+
142 2019 INSC 218 2
|
| 1422 |
+
142 2020 INSC 392 2
|
| 1423 |
+
142 2022 INSC 1043 2
|
| 1424 |
+
143 1996 INSC 237 3
|
| 1425 |
+
143 2018 INSC 804 2
|
| 1426 |
+
143 2019 INSC 651 2
|
| 1427 |
+
143 2024 INSC 178 2
|
| 1428 |
+
143 2024 INSC 545 2
|
| 1429 |
+
143 2013 INSC 528 2
|
| 1430 |
+
143 2025 INSC 491 2
|
| 1431 |
+
143 2012 INSC 486 2
|
| 1432 |
+
143 2014 INSC 48 2
|
| 1433 |
+
144 2002 INSC 203 3
|
| 1434 |
+
144 2018 INSC 288 2
|
| 1435 |
+
144 2018 INSC 437 2
|
| 1436 |
+
144 2019 INSC 1007 2
|
| 1437 |
+
144 2021 INSC 650 2
|
| 1438 |
+
144 2021 INSC 332 2
|
| 1439 |
+
144 2024 INSC 150 2
|
| 1440 |
+
144 2004 INSC 502 2
|
| 1441 |
+
144 2011 INSC 626 2
|
| 1442 |
+
144 2012 INSC 342 2
|
| 1443 |
+
144 2012 INSC 382 2
|
| 1444 |
+
145 1994 INSC 348 3
|
| 1445 |
+
145 2017 INSC 591 2
|
| 1446 |
+
145 2023 INSC 249 2
|
| 1447 |
+
145 1995 INSC 272 2
|
| 1448 |
+
145 2005 INSC 416 2
|
| 1449 |
+
145 2008 INSC 867 2
|
| 1450 |
+
145 2011 INSC 737 2
|
| 1451 |
+
145 2011 INSC 788 2
|
| 1452 |
+
146 1962 INSC 289 3
|
| 1453 |
+
146 2017 INSC 26 2
|
| 1454 |
+
146 2025 INSC 684 2
|
| 1455 |
+
146 2025 INSC 1130 2
|
| 1456 |
+
146 2006 INSC 681 2
|
| 1457 |
+
146 2010 INSC 204 2
|
| 1458 |
+
147 2011 INSC 301 3
|
| 1459 |
+
147 2016 INSC 1133 2
|
| 1460 |
+
147 2016 INSC 948 2
|
| 1461 |
+
147 2016 INSC 608 2
|
| 1462 |
+
147 2017 INSC 1038 2
|
| 1463 |
+
147 2018 INSC 1184 2
|
| 1464 |
+
147 2020 INSC 498 2
|
| 1465 |
+
147 2020 INSC 497 2
|
| 1466 |
+
147 2020 INSC 711 2
|
| 1467 |
+
147 2020 INSC 697 2
|
| 1468 |
+
147 2021 INSC 216 2
|
| 1469 |
+
148 1961 INSC 196 3
|
| 1470 |
+
148 2022 INSC 105 2
|
| 1471 |
+
148 2023 INSC 613 2
|
| 1472 |
+
148 1970 INSC 256 2
|
| 1473 |
+
148 2008 INSC 876 2
|
| 1474 |
+
149 1997 INSC 622 3
|
| 1475 |
+
149 2019 INSC 597 2
|
| 1476 |
+
149 2020 INSC 656 2
|
| 1477 |
+
149 2022 INSC 322 2
|
| 1478 |
+
150 2001 INSC 294 3
|
| 1479 |
+
150 2019 INSC 420 2
|
| 1480 |
+
150 2019 INSC 706 2
|
| 1481 |
+
150 2020 INSC 682 2
|
| 1482 |
+
150 2020 INSC 400 2
|
| 1483 |
+
150 2021 INSC 160 2
|
| 1484 |
+
150 2021 INSC 200 2
|
| 1485 |
+
150 2024 INSC 324 2
|
| 1486 |
+
150 2013 INSC 224 2
|
phase1/eval/authority_queries.tsv
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
1 authority can daily wage or temporary employees claim a right to regularization or permanent absorption in government service
|
| 2 |
+
2 authority can the Supreme Court depart from or overrule its own previous decision when satisfied of error
|
| 3 |
+
3 authority whether 'or' in section 24(2) between possession not taken and compensation not paid is read conjunctively as 'nor' so both conditions must be unmet for deemed lapse of acquisition
|
| 4 |
+
4 authority conferring uncontrolled discretion on government to pick cases for special court procedure without classification violates Article 14 equality
|
| 5 |
+
5 authority selection of multiplier and deduction for personal living expenses in computing motor accident compensation for deceased
|
| 6 |
+
6 authority constitutional validity of TADA and whether vague terrorism offence provisions violate fundamental rights
|
| 7 |
+
7 authority extent of government regulation over admissions and administration of private unaided and minority educational institutions
|
| 8 |
+
8 authority whether deposit of compensation in government treasury instead of court amounts to compensation paid under section 24(2) deemed lapse
|
| 9 |
+
9 authority scope of High Court power to reverse an acquittal in appeal and when two views are possible the one favouring the accused
|
| 10 |
+
10 authority scope of judicial review of government tender and contract award decisions under Article 14
|
| 11 |
+
11 authority can the Supreme Court suspend an advocate's licence to practice while punishing him for criminal contempt of court under Article 129 and 142
|
| 12 |
+
12 authority doctrine of merger applicability when special leave petition is dismissed in limine versus dismissed by a speaking order after grant of leave
|
| 13 |
+
13 authority constitutional validity of reservation in promotion with consequential seniority and the requirement of quantifiable data on backwardness inadequacy and efficiency
|
| 14 |
+
14 authority whether power of judicial review of High Courts under Article 226 and Supreme Court under Article 32 is part of the basic structure and can tribunals exclude it
|
| 15 |
+
15 authority categories and scope of inherent power of the High Court to quash criminal proceedings and FIR to prevent abuse of process
|
| 16 |
+
16 authority whether courts can award imprisonment for life with no remission for full term as an alternative to death penalty in cases short of rarest of rare
|
| 17 |
+
17 authority nature of the power of the Chief Justice to appoint an arbitrator under section 11 whether administrative or judicial and what issues he can decide
|
| 18 |
+
18 authority constitutional validity of the Insolvency and Bankruptcy Code and whether classification between financial creditors and operational creditors violates Article 14
|
| 19 |
+
19 authority addition for future prospects to income of deceased and standardized percentages for permanent job self employed and fixed salary in motor accident compensation
|
| 20 |
+
20 authority whether the right to privacy is a fundamental right protected under Article 21 as part of the right to life and personal liberty
|
| 21 |
+
21 authority duty of court to take active participatory role in collecting evidence and ensure a fair trial when witnesses turn hostile
|
| 22 |
+
22 authority whether High Court judges elevated from the Bar are entitled to same full pension as judges drawn from judicial service by adding notional service period
|
| 23 |
+
23 authority binding guidelines to prevent sexual harassment of women at the workplace in absence of legislation
|
| 24 |
+
24 authority ingredients to be proved for murder under section 300 thirdly intention to inflict the particular bodily injury sufficient in ordinary course of nature to cause death
|
| 25 |
+
25 authority scope of executive power of the State whether prior legislation is required for executive to act under Articles 73 and 162
|
| 26 |
+
26 authority whether conviction can be based on uncorroborated testimony of a single witness and classification of witnesses as reliable or unreliable
|
| 27 |
+
27 authority extent to which pleasure doctrine of holding office under Article 310 is subject to procedural safeguards and statutory rules in dismissal of public servant
|
| 28 |
+
28 authority power of appellate court to reverse an order of acquittal and re-appreciate evidence and adverse inference from false answers under section 313
|
| 29 |
+
29 authority stage and scope of power to summon additional accused under section 319 CrPC meaning of inquiry and trial
|
| 30 |
+
30 authority whether a decision of a coordinate bench is binding on a later bench of equal strength and doctrine of binding precedent
|
| 31 |
+
31 authority whether a state sales tax law that imposes tax on inter-State sales is merely dormant until Parliament lifts the ban under Article 286(2) or wholly void
|
| 32 |
+
32 authority can laws placed in the Ninth Schedule after 24 April 1973 be challenged for violating the basic structure and fundamental rights under Articles 14, 19 and 21
|
| 33 |
+
33 authority does mere non-deposit of compensation in court under the 1894 Act cause land acquisition to lapse under section 24(2), or does 'paid' mean tender of payment
|
| 34 |
+
34 authority how must courts weigh aggravating and mitigating circumstances and apply the rarest of rare doctrine before imposing the death penalty
|
| 35 |
+
35 authority test for repugnancy under Article 254 when Parliament and a State legislature both legislate under a Concurrent List entry occupying different fields
|
| 36 |
+
36 authority whether prize competitions involving gambling are protected as trade or business under Article 19(1)(g) and severability of a statute partly invalid
|
| 37 |
+
37 authority is a delinquent employee entitled to a copy of the inquiry officer's report before the disciplinary authority imposes punishment
|
| 38 |
+
38 authority what constitutes a substantial question of law required for admission of a second appeal under section 100 CPC
|
| 39 |
+
39 authority is registration of an FIR under section 154 mandatory when information discloses a cognizable offence or can the police hold a preliminary inquiry first
|
| 40 |
+
40 authority grounds on which a statute can be struck down as arbitrary and whether arbitrariness alone violates Article 14
|
| 41 |
+
41 authority whether Article 31A protection for acquisition of estates is limited to agrarian reform legislation
|
| 42 |
+
42 authority power of Election Commission under Article 324 to direct candidates to disclose criminal antecedents and assets
|
| 43 |
+
43 authority right of appeal is a vested substantive right governed by the law in force on the date of institution of the suit
|
| 44 |
+
44 authority whether courts can interfere with the election process before declaration of result or only by election petition under Article 329(b)
|
| 45 |
+
45 authority bar under Section 195(1)(b)(ii) CrPC applies only to forgery of a document committed while it is in custodia legis
|
| 46 |
+
46 authority whether separate charges and trials can be framed for distinct offences in a single conspiracy spanning different years
|
| 47 |
+
47 authority constitutional validity of enforcement of security interest without court intervention under SARFAESI Section 13
|
| 48 |
+
48 authority mandatory compliance with Section 50 NDPS Act right to be searched before a Gazetted Officer or Magistrate
|
| 49 |
+
49 authority whether auction is the only constitutionally permissible method for alienation of natural resources by the State
|
| 50 |
+
50 authority voter's right to know antecedents of candidates as a facet of Article 19(1)(a) and whether legislature can nullify it
|
| 51 |
+
51 authority is the right to education a fundamental right flowing from the right to life under article 21
|
| 52 |
+
52 authority can an arbitral award be set aside as against public policy of india for patent illegality
|
| 53 |
+
53 authority distinction between regulation of property under article 19(1)(f) and deprivation of property under article 31
|
| 54 |
+
54 authority can freedom of speech be restricted only to protect security of the state and not general public order
|
| 55 |
+
55 authority constitutional validity of 27 percent OBC reservation in central educational institutions and exclusion of creamy layer
|
| 56 |
+
56 authority does an illegal investigation in breach of mandatory provisions vitiate the trial without miscarriage of justice
|
| 57 |
+
57 authority strict interpretation of tax exemption notification and whether ambiguity benefits revenue or assessee
|
| 58 |
+
58 authority power of high court under section 482 to quash non-compoundable criminal proceedings on settlement between parties
|
| 59 |
+
59 authority validity of an administrative order judged only by reasons stated in the order itself
|
| 60 |
+
60 authority principles for grant and cancellation of bail and need for reasoned order exercising judicial discretion
|
| 61 |
+
61 authority precautionary principle and polluter pays principle as part of Indian environmental law sustainable development burden of proof on industry
|
| 62 |
+
62 authority scope of certiorari and judicial review over decisions of highest statutory appellate authority error within jurisdiction natural justice
|
| 63 |
+
63 authority distinction between tax and fee quid pro quo element of service rendered constitutional validity of cess
|
| 64 |
+
64 authority whether time limit to file written statement under Order VIII Rule 1 CPC is mandatory or directory power of court to extend time
|
| 65 |
+
65 authority service conditions and pay scales of subordinate judiciary parity of judicial officers with executive judicial pay commission
|
| 66 |
+
66 authority insurer's liability third party claim breach of policy condition driver without valid licence burden on insurer to prove breach contributed to accident
|
| 67 |
+
67 authority quashing criminal proceedings on ground of pending civil dispute breach of contract whether civil remedy bars criminal prosecution
|
| 68 |
+
68 authority allocation of scarce natural resources spectrum first come first served versus auction transparency arbitrariness Article 14
|
| 69 |
+
69 authority vicarious liability of director under Section 141 Negotiable Instruments Act necessity of specific averment in cheque dishonour complaint
|
| 70 |
+
70 authority reasonable classification under Article 14 permissible differentiation versus discrimination intelligible differentia special courts
|
| 71 |
+
71 authority can a writ petition under Article 226 be entertained to challenge a show cause notice when an alternative statutory remedy is available
|
| 72 |
+
72 authority extent of state regulation over admissions and fee structure in unaided minority and private professional educational institutions under Articles 29 and 30
|
| 73 |
+
73 authority power of states to levy tax on mineral bearing land under Entry 49 List II despite Centre's regulation of mines under the MMDR Act
|
| 74 |
+
74 authority what constitutes abetment of suicide under Section 306 IPC and whether mere harassment amounts to instigation
|
| 75 |
+
75 authority is residence in the state a constitutional requirement for election to the Rajya Sabha and validity of open ballot in Council of States elections
|
| 76 |
+
76 authority does a sentence of imprisonment for life mean imprisonment for the whole remaining life unless remitted or commuted by the appropriate government
|
| 77 |
+
77 authority whether a taxing statute imposing a flat rate of tax without classification and irrespective of income violates Article 14
|
| 78 |
+
78 authority maintainability of a curative petition to reconsider a final Supreme Court judgment after dismissal of review under inherent powers and Article 142
|
| 79 |
+
79 authority quashing of criminal proceedings under Section 482 CrPC where a civil dispute is given the cloak of a criminal offence
|
| 80 |
+
80 authority whether courts can supply a casus omissus and read words into a plain and unambiguous statutory provision
|
| 81 |
+
81 authority is requiring prior government sanction before CBI inquiry against senior officials a valid Article 14 classification under the Prevention of Corruption Act
|
| 82 |
+
82 authority constitutionality of reverse burden of proof and presumption provisions under the NDPS Act against presumption of innocence
|
| 83 |
+
83 authority right of private unaided professional colleges to fix their own fee structure and admission procedure and need for state regulatory committees
|
| 84 |
+
84 authority scope of court's power under section 11(6A) confined to examining existence of arbitration agreement at the appointment stage
|
| 85 |
+
85 authority when can a court or tribunal interfere with quantum of disciplinary penalty imposed by the disciplinary authority
|
| 86 |
+
86 authority is section 377 criminalising consensual same-sex acts between adults unconstitutional under Articles 14 and 21
|
| 87 |
+
87 authority scope of judicial review over Prime Minister's discretion to appoint persons with criminal antecedents as ministers
|
| 88 |
+
88 authority harmonising denominational temple management rights under Article 26(b) with public right of temple entry under Article 25(2)(b)
|
| 89 |
+
89 authority doctrine of repugnancy where a central law evinces intention to occupy the entire field overriding state legislation
|
| 90 |
+
90 authority rarest of rare doctrine and balancing crime against criminal in awarding death penalty
|
| 91 |
+
91 authority can employees who accept voluntary retirement scheme later claim revision of pay scale from a back date
|
| 92 |
+
92 authority grounds for cancellation of bail granted by sessions judge without reasons in dowry death case
|
| 93 |
+
93 authority scope of right to property under Article 300A and deprivation of property only by authority of law with public purpose
|
| 94 |
+
94 authority single shareholder locus standi to file writ petition under Article 32 challenging acquisition of company property
|
| 95 |
+
95 authority whether section 5 of Limitation Act applies to delay in filing application to set aside arbitral award beyond the prescribed period under section 34
|
| 96 |
+
96 authority credibility of interested or related witnesses and applicability of falsus in uno falsus in omnibus in criminal trial
|
| 97 |
+
97 authority applicability of Part I of Arbitration Act to foreign seated international commercial arbitration and interim relief under section 9
|
| 98 |
+
98 authority evidentiary value of confession of co-accused recorded under TADA and its use against other accused
|
| 99 |
+
99 authority scope of High Court interference in acquittal appeal reappreciating evidence where trial court view is reasonable
|
| 100 |
+
100 authority relevant date for determining age of juvenile offender date of offence or date of production before court
|
| 101 |
+
101 authority standard of care for criminal negligence against a doctor under section 304A and the requirement of gross negligence
|
| 102 |
+
102 authority factors for choosing between death penalty and life imprisonment based on character antecedents and reformability of the offender
|
| 103 |
+
103 authority test for what constitutes an industry under the Industrial Disputes Act and application of noscitur a sociis to the definition
|
| 104 |
+
104 authority where a statute prescribes the manner of doing an act it must be done in that manner or not at all
|
| 105 |
+
105 authority whether Article 166 of the Constitution on authentication of government orders is directory or mandatory
|
| 106 |
+
106 authority whether a director or signatory can be prosecuted under section 138 cheque dishonour without the company being arraigned as accused
|
| 107 |
+
107 authority requirement of hearing and recording of satisfaction for invoking urgency clause and dispensing with enquiry in land acquisition
|
| 108 |
+
108 authority whether rules of pleading and burden of proof apply to public interest litigation
|
| 109 |
+
109 authority binding effect of precedent and duty of coordinate and smaller benches to follow larger bench decisions under stare decisis
|
| 110 |
+
110 authority whether a dying declaration can be the sole basis for conviction without corroboration
|
| 111 |
+
111 authority do High Courts and the Supreme Court as courts of record have inherent power to punish contempt of subordinate courts
|
| 112 |
+
112 authority what does conscious possession of unauthorised arms in a notified area require under TADA section 5 and is the presumption rebuttable
|
| 113 |
+
113 authority can courts impose a special category life sentence beyond remission for a fixed term exceeding fourteen years instead of death penalty
|
| 114 |
+
114 authority burden on accused to explain wife's unnatural death in matrimonial home under section 106 Evidence Act in circumstantial evidence cases
|
| 115 |
+
115 authority whether a beneficial amendment during pendency of appeal applies retrospectively to defeat a vested right that accrued on the trial court decree
|
| 116 |
+
116 authority scope of judicial review of viva voce interview marks and selection process in public service recruitment
|
| 117 |
+
117 authority whether freedom of trade commerce and intercourse under Article 301 includes freedom from tax laws restricting movement of goods
|
| 118 |
+
118 authority when can an appellate court reverse an acquittal in a case based on circumstantial evidence and the only-perverse-view standard
|
| 119 |
+
119 authority when can an arbitral award be set aside under section 34 for breach of natural justice or conflict with the fundamental policy of Indian law after the 2015 amendment
|
| 120 |
+
120 authority requirements for reconversion to claim scheduled caste status and acceptance by the community
|
| 121 |
+
121 authority if landowner fails to file objections under Section 5A can he later challenge the land acquisition declaration
|
| 122 |
+
122 authority powers and principles for appellate court to interfere with and reverse an order of acquittal
|
| 123 |
+
123 authority conviction in rape case on sole uncorroborated testimony of prosecutrix and justification for delay in filing FIR
|
| 124 |
+
124 authority twin conditions for grant of bail under MCOCA and meaning of reasonable grounds to believe accused not guilty
|
| 125 |
+
125 authority whether doctrine of eclipse applies to a post-constitution law violating fundamental rights
|
| 126 |
+
126 authority can bail be granted on ground of parity to a history-sheeter habitual offender
|
| 127 |
+
127 authority are homebuyers financial creditors under IBC and does the Code prevail over RERA
|
| 128 |
+
128 authority permissible ceiling on reservation and caste as sole basis under Article 15(4)
|
| 129 |
+
129 authority commercial wisdom of committee of creditors and limited scope of judicial review of resolution plan under IBC
|
| 130 |
+
130 authority applicability of Part I of Arbitration Act to international commercial arbitration held outside India
|
| 131 |
+
131 authority can roster point reserved category promotees count seniority over senior general candidates who reach the promotional level later; is reservation in promotion under Article 16(4A) a fundamental right or only an enabling provision
|
| 132 |
+
132 authority is an oral hearing in open court mandatory at the review petition stage in death sentence cases under Article 21
|
| 133 |
+
133 authority does the Juvenile Justice Act apply to determine juvenility on the date of commission of the offence even in pending or already concluded cases; how is age of juvenility determined
|
| 134 |
+
134 authority does life imprisonment mean imprisonment for the whole of the convict's natural life subject only to statutory remission and constitutional clemency powers
|
| 135 |
+
135 authority is non-joinder of a necessary party or non-compliance with Section 82 of the Representation of the People Act fatal to an election petition, or is such a provision directory rather than mandatory
|
| 136 |
+
136 authority can the validity of regulations framed by a statutory regulator under delegated legislative power be challenged before the appellate tribunal or only by judicial review; distinction between an order and a regulation
|
| 137 |
+
137 authority is a land reform statute placed in the Ninth Schedule immune from challenge for failure to provide compensation for acquisition of property under Article 31
|
| 138 |
+
138 authority when offence is punishable up to seven years can police arrest automatically; mandatory safeguards and recording of reasons under Section 41 and 41A CrPC before arrest in Section 498A cases
|
| 139 |
+
139 authority do declaratory and explanatory amending statutes operate retrospectively; does substitution of a statutory provision amount to repeal and re-enactment for retrospective effect
|
| 140 |
+
140 authority is the exercise of a minister's discretionary quota in allotment subject to judicial review for arbitrariness and abuse of discretion in public law
|
| 141 |
+
141 authority if a party who was set ex parte appears at a later adjourned hearing of a suit, can the court let him participate and from what stage do proceedings continue under Order 9 Rule 7 CPC
|
| 142 |
+
142 authority is an interim or partial award under the Arbitration Act final and challengeable under Section 34, and can the arbitrator's interpretation of the contract including questions of law be interfered with
|
| 143 |
+
143 authority polluter pays principle and absolute liability of hazardous chemical industries to bear the cost of remediation, and power of Central Government to recover remedial costs under the Environment Protection Act
|
| 144 |
+
144 authority can courts fix outer time limits or bars of limitation for conclusion of criminal trials to enforce the right to speedy trial, or is that impermissible judicial legislation
|
| 145 |
+
145 authority procedure and guidelines for issuance, scrutiny and verification of caste or scheduled tribe status certificates and consequences of admissions obtained on a false caste certificate
|
| 146 |
+
146 authority meaning of manufacture for levy of excise duty and whether processing that does not bring into existence a new and distinct marketable commodity amounts to manufacture
|
| 147 |
+
147 authority which categories of disputes are non-arbitrable and when will a court refuse a Section 8 reference to arbitration because the subject matter is reserved for a public forum
|
| 148 |
+
148 authority does the reservation power under Article 16(4) extend to reservation in promotions to selection posts and not merely initial appointments
|
| 149 |
+
149 authority scope of judicial review of administrative action on grounds of unreasonableness and irrationality and applicability of the doctrine of proportionality in India
|
| 150 |
+
150 authority whether a second FIR can be registered for the same cognizable offence arising out of the same transaction under Section 154 CrPC
|
phase1/eval/authority_sample.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
phase1/eval/bad_law_docids.txt
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
2019 INSC 372
|
| 2 |
+
2015 INSC 793
|
| 3 |
+
2015 INSC 52
|
| 4 |
+
2015 INSC 160
|
| 5 |
+
2015 INSC 235
|
| 6 |
+
2015 INSC 257
|
| 7 |
+
2016 INSC 375
|
| 8 |
+
2016 INSC 491
|
| 9 |
+
2016 INSC 526
|
| 10 |
+
2017 INSC 1026
|
| 11 |
+
2017 INSC 1092
|
| 12 |
+
2017 INSC 121
|
| 13 |
+
2017 INSC 102
|
| 14 |
+
2017 INSC 899
|
| 15 |
+
2018 INSC 896
|
| 16 |
+
2018 INSC 732
|
| 17 |
+
2018 INSC 84
|
| 18 |
+
2018 INSC 405
|
| 19 |
+
2018 INSC 714
|
| 20 |
+
2019 INSC 1184
|
| 21 |
+
2019 INSC 511
|
| 22 |
+
2020 INSC 294
|
| 23 |
+
2021 INSC 314
|
| 24 |
+
2022 INSC 1312
|
| 25 |
+
1960 INSC 107
|
| 26 |
+
1960 INSC 123
|
| 27 |
+
1960 INSC 195
|
| 28 |
+
1960 INSC 200
|
| 29 |
+
1961 INSC 177
|
| 30 |
+
1962 INSC 247
|
| 31 |
+
1962 INSC 389
|
| 32 |
+
1962 INSC 328
|
| 33 |
+
1964 INSC 7
|
| 34 |
+
1964 INSC 27
|
| 35 |
+
1964 INSC 203
|
| 36 |
+
1964 INSC 206
|
| 37 |
+
1965 INSC 154
|
| 38 |
+
1966 INSC 155
|
| 39 |
+
1967 INSC 45
|
| 40 |
+
1967 INSC 87
|
| 41 |
+
1967 INSC 122
|
| 42 |
+
1967 INSC 173
|
| 43 |
+
1968 INSC 72
|
| 44 |
+
1969 INSC 8
|
| 45 |
+
1969 INSC 20
|
| 46 |
+
1969 INSC 77
|
| 47 |
+
1969 INSC 99
|
| 48 |
+
1969 INSC 87
|
| 49 |
+
1970 INSC 18
|
| 50 |
+
1970 INSC 190
|
| 51 |
+
1971 INSC 20
|
| 52 |
+
1974 INSC 256
|
| 53 |
+
1975 INSC 212
|
| 54 |
+
1976 INSC 270
|
| 55 |
+
1976 INSC 231
|
| 56 |
+
1976 INSC 250
|
| 57 |
+
1976 INSC 272
|
| 58 |
+
1977 INSC 28
|
| 59 |
+
1977 INSC 75
|
| 60 |
+
1977 INSC 155
|
| 61 |
+
1978 INSC 16
|
| 62 |
+
1981 INSC 175
|
| 63 |
+
1981 INSC 211
|
| 64 |
+
1981 INSC 209
|
| 65 |
+
1983 INSC 10
|
| 66 |
+
1984 INSC 67
|
| 67 |
+
1984 INSC 152
|
| 68 |
+
1985 INSC 101
|
| 69 |
+
1987 INSC 259
|
| 70 |
+
1988 INSC 61
|
| 71 |
+
1989 INSC 54
|
| 72 |
+
1996 INSC 90
|
| 73 |
+
1996 INSC 800
|
| 74 |
+
1997 INSC 441
|
| 75 |
+
1998 INSC 185
|
| 76 |
+
2001 INSC 158
|
| 77 |
+
2002 INSC 66
|
| 78 |
+
2002 INSC 123
|
| 79 |
+
2004 INSC 34
|
| 80 |
+
2004 INSC 182
|
| 81 |
+
2005 INSC 146
|
| 82 |
+
2007 INSC 1026
|
| 83 |
+
2007 INSC 28
|
| 84 |
+
2007 INSC 475
|
| 85 |
+
2007 INSC 772
|
| 86 |
+
2008 INSC 930
|
| 87 |
+
2008 INSC 82
|
| 88 |
+
2008 INSC 677
|
| 89 |
+
2009 INSC 946
|
| 90 |
+
2009 INSC 1045
|
| 91 |
+
2009 INSC 1195
|
| 92 |
+
2009 INSC 209
|
| 93 |
+
2010 INSC 69
|
| 94 |
+
2010 INSC 177
|
| 95 |
+
2011 INSC 508
|
| 96 |
+
2012 INSC 49
|
| 97 |
+
2013 INSC 830
|
| 98 |
+
2013 INSC 684
|
| 99 |
+
2013 INSC 823
|
| 100 |
+
2013 INSC 377
|
| 101 |
+
2013 INSC 494
|
| 102 |
+
2014 INSC 218
|
| 103 |
+
2014 INSC 617
|
| 104 |
+
2014 INSC 579
|
phase1/eval/batched_run.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GPU-BATCHED retrieval harness: encode ALL queries at once, one big dense matmul, and ONE batched
|
| 2 |
+
cross-encoder pass over every (query, candidate) pair — so the GPU is actually saturated. Turns a
|
| 3 |
+
~10min serial run into ~30s. Same knobs (CAND, ALPHA). Emits run.tsv."""
|
| 4 |
+
import json, os, time
|
| 5 |
+
import numpy as np
|
| 6 |
+
from sentence_transformers import SentenceTransformer, CrossEncoder
|
| 7 |
+
DATA = os.environ.get("THEMIS_DATA", ".")
|
| 8 |
+
EVAL = os.environ.get("THEMIS_EVAL", ".")
|
| 9 |
+
DEVICE = os.environ.get("THEMIS_DEVICE", "cuda")
|
| 10 |
+
CAND = int(os.environ.get("CAND", "40"))
|
| 11 |
+
ALPHA = float(os.environ.get("ALPHA", "0"))
|
| 12 |
+
CE_BATCH = int(os.environ.get("CE_BATCH", "512"))
|
| 13 |
+
BGE_Q = "Represent this sentence for searching relevant passages: "
|
| 14 |
+
|
| 15 |
+
print("loading chunks/vectors/models ...", flush=True)
|
| 16 |
+
texts = []; chunk_doc = []
|
| 17 |
+
with open(os.path.join(DATA, "escr_chunks.jsonl"), encoding="utf-8") as f:
|
| 18 |
+
for l in f:
|
| 19 |
+
c = json.loads(l); texts.append(c["text"]); chunk_doc.append(c["doc_id"])
|
| 20 |
+
M = np.load(os.path.join(DATA, "escr_vectors.npy")) # (nchunks, 384) float32
|
| 21 |
+
cite_indeg = {}
|
| 22 |
+
if ALPHA:
|
| 23 |
+
from collections import Counter
|
| 24 |
+
ci = Counter()
|
| 25 |
+
with open(os.path.join(DATA, "edges.jsonl"), encoding="utf-8") as f:
|
| 26 |
+
for l in f:
|
| 27 |
+
e = json.loads(l)
|
| 28 |
+
if e.get("method") == "cite": ci[e["target"]] += 1
|
| 29 |
+
cite_indeg = ci
|
| 30 |
+
st = SentenceTransformer("BAAI/bge-small-en-v1.5", device=DEVICE)
|
| 31 |
+
ce = CrossEncoder(os.environ.get("THEMIS_RERANKER","cross-encoder/ms-marco-MiniLM-L-6-v2"), device=DEVICE)
|
| 32 |
+
print(f"ready (CAND={CAND} ALPHA={ALPHA} dev={DEVICE})", flush=True)
|
| 33 |
+
|
| 34 |
+
QFILE = os.environ.get("THEMIS_QFILE", os.path.join(EVAL, "queries.tsv"))
|
| 35 |
+
OUT = os.environ.get("THEMIS_OUT", os.path.join(EVAL, "run.tsv"))
|
| 36 |
+
qids = []; qtexts = []
|
| 37 |
+
with open(QFILE, encoding="utf-8") as f:
|
| 38 |
+
for l in f:
|
| 39 |
+
qid, intent, text = l.rstrip("\n").split("\t", 2); qids.append(qid); qtexts.append(text)
|
| 40 |
+
nq = len(qids)
|
| 41 |
+
t0 = time.time()
|
| 42 |
+
# 1) batch-encode all queries, 2) dense top-CAND per query (chunked over queries to bound RAM)
|
| 43 |
+
Q = st.encode([BGE_Q + q for q in qtexts], batch_size=256, normalize_embeddings=True, convert_to_numpy=True).astype(np.float32)
|
| 44 |
+
cand_idx = np.empty((nq, CAND), dtype=np.int64)
|
| 45 |
+
STEP = 200
|
| 46 |
+
for s in range(0, nq, STEP):
|
| 47 |
+
sims = M @ Q[s:s+STEP].T # (nchunks, b)
|
| 48 |
+
for j in range(sims.shape[1]):
|
| 49 |
+
col = sims[:, j]; top = np.argpartition(-col, CAND)[:CAND]
|
| 50 |
+
cand_idx[s+j] = top[np.argsort(-col[top])]
|
| 51 |
+
print(f"dense done {time.time()-t0:.0f}s", flush=True)
|
| 52 |
+
# 3) ONE batched cross-encoder pass over all (query, candidate-chunk) pairs
|
| 53 |
+
pairs = []; owner = []
|
| 54 |
+
for i in range(nq):
|
| 55 |
+
for ci_ in cand_idx[i]:
|
| 56 |
+
pairs.append((qtexts[i], texts[ci_])); owner.append(i)
|
| 57 |
+
rr = ce.predict(pairs, batch_size=CE_BATCH, show_progress_bar=True)
|
| 58 |
+
print(f"rerank done {time.time()-t0:.0f}s", flush=True)
|
| 59 |
+
# 4) best chunk per doc per query, blend authority, rank
|
| 60 |
+
def _sig(x): return 1.0 / (1.0 + np.exp(-x))
|
| 61 |
+
best = [dict() for _ in range(nq)]
|
| 62 |
+
for k, s in enumerate(rr):
|
| 63 |
+
i = owner[k]; d = chunk_doc[cand_idx[i][k % CAND]]
|
| 64 |
+
if d not in best[i] or s > best[i][d]: best[i][d] = float(s)
|
| 65 |
+
with open(OUT, "w", encoding="utf-8") as f:
|
| 66 |
+
for i in range(nq):
|
| 67 |
+
scored = [(_sig(s) + (ALPHA * np.log1p(cite_indeg.get(d, 0)) if ALPHA else 0.0), d) for d, s in best[i].items()]
|
| 68 |
+
scored.sort(reverse=True)
|
| 69 |
+
for rank, (_, d) in enumerate(scored[:20], 1):
|
| 70 |
+
f.write(f"{qids[i]}\t{rank}\t{d}\n")
|
| 71 |
+
print(f"done {nq} queries in {time.time()-t0:.0f}s -> run.tsv", flush=True)
|
phase1/eval/bench_queries.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id": "fact-1", "intent": "fact", "query": "husband and his family harassing wife for dowry, can the FIR under section 498A be quashed if the parties reach a settlement"},
|
| 3 |
+
{"id": "fact-2", "intent": "fact", "query": "cheque issued on behalf of a company was dishonoured, is the director personally liable under section 138 of the Negotiable Instruments Act"},
|
| 4 |
+
{"id": "fact-3", "intent": "fact", "query": "employee dismissed from service without a departmental inquiry, whether the termination violates principles of natural justice"},
|
| 5 |
+
{"id": "fact-4", "intent": "fact", "query": "accused seeking anticipatory bail in an economic offence involving diversion of investor money"},
|
| 6 |
+
{"id": "fact-5", "intent": "fact", "query": "government acquired private land and the owner claims the compensation awarded is far below the market value"},
|
| 7 |
+
{"id": "fact-6", "intent": "fact", "query": "person contracted a second marriage while the divorce petition from the first marriage was still pending, validity and bigamy"},
|
| 8 |
+
{"id": "issue-1", "intent": "issue", "query": "whether a dying declaration alone, without corroboration, is sufficient to sustain a conviction"},
|
| 9 |
+
{"id": "issue-2", "intent": "issue", "query": "the tests for grant of a temporary injunction: prima facie case, balance of convenience and irreparable injury"},
|
| 10 |
+
{"id": "issue-3", "intent": "issue", "query": "scope of judicial review of administrative action on the ground of arbitrariness under Article 14"},
|
| 11 |
+
{"id": "issue-4", "intent": "issue", "query": "whether bail once granted can be cancelled merely on the basis of subsequent developments"},
|
| 12 |
+
{"id": "vague-1", "intent": "vague", "query": "the supreme court judgment holding that privacy is a fundamental right, connected with the aadhaar matter"},
|
| 13 |
+
{"id": "vague-2", "intent": "vague", "query": "constitution bench decision on reservation in promotion for scheduled caste and scheduled tribe employees"},
|
| 14 |
+
{"id": "citation-1", "intent": "citation", "query": "(2017) 10 SCC 1"},
|
| 15 |
+
{"id": "casename-1", "intent": "casename", "query": "K.S. Puttaswamy v Union of India"},
|
| 16 |
+
{"id": "casename-2", "intent": "casename", "query": "Vishaka v State of Rajasthan"}
|
| 17 |
+
]
|
phase1/eval/build_held_vectors.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build doc-level HELD embeddings (the $0 representation win): one vector per judgment that has a
|
| 2 |
+
HELD headnote (~56%), same BGE-small space. Output: held_vectors.npy + held_docids.json."""
|
| 3 |
+
import json, os, time
|
| 4 |
+
import numpy as np
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
DATA = os.environ.get("THEMIS_DATA", ".")
|
| 7 |
+
docs = []; texts = []
|
| 8 |
+
for l in open(os.path.join(DATA, "escr_meta.jsonl"), encoding="utf-8"):
|
| 9 |
+
m = json.loads(l); h = (m.get("held") or "").strip()
|
| 10 |
+
if len(h) > 40:
|
| 11 |
+
docs.append(m["doc_id"]); texts.append(h[:1800])
|
| 12 |
+
print(f"{len(docs)} judgments with HELD", flush=True)
|
| 13 |
+
st = SentenceTransformer("BAAI/bge-small-en-v1.5", device="cpu")
|
| 14 |
+
t0 = time.time()
|
| 15 |
+
V = st.encode(texts, batch_size=128, normalize_embeddings=True, convert_to_numpy=True).astype(np.float32)
|
| 16 |
+
np.save(os.path.join(DATA, "held_vectors.npy"), V)
|
| 17 |
+
json.dump(docs, open(os.path.join(DATA, "held_docids.json"), "w"))
|
| 18 |
+
print(f"DONE {V.shape} in {time.time()-t0:.0f}s -> held_vectors.npy", flush=True)
|
phase1/eval/build_qrels.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the v1 MECHANICAL eval set (zero model, zero human, leakage-controlled).
|
| 2 |
+
Outputs (TREC-style, frozen):
|
| 3 |
+
queries.tsv qid \\t intent \\t query_text
|
| 4 |
+
qrels.tsv qid \\t doc_id \\t grade (graded 0/1/2/3; here single grade-3 target per query)
|
| 5 |
+
bad_law_docids.txt confirmed-overruled/doubted/per_incuriam doc_ids (the precision guardrail deny-list)
|
| 6 |
+
|
| 7 |
+
Slices:
|
| 8 |
+
citing_passage ~500 query = a citing court's snippet where it RELIED ON / FOLLOWED a case,
|
| 9 |
+
with the cited case's NAME + CITATIONS stripped out (anti-leak); gold = that case (grade 3).
|
| 10 |
+
Ground truth authored by real SC benches (edges.jsonl strong-positive treatments).
|
| 11 |
+
known_item_cite ~150 query = a neutral citation; gold = its own doc (grade 3). success@1 control.
|
| 12 |
+
known_item_name ~150 query = a case name; gold = its own doc (grade 3). success@1 control.
|
| 13 |
+
"""
|
| 14 |
+
import json, re, random, os
|
| 15 |
+
random.seed(13)
|
| 16 |
+
DATA = "/Users/gongura/Code/themis/phase1/data/thor_artifacts"
|
| 17 |
+
OUT = "/Users/gongura/Code/themis/phase1/eval"
|
| 18 |
+
os.makedirs(OUT, exist_ok=True)
|
| 19 |
+
|
| 20 |
+
STOP = set("the of and v vs versus state union india ltd co anr ors etc rep by".split())
|
| 21 |
+
FUNC = set("the of a an that which is are was were to in on for by with as has have had not no be been it this these those under where when whether while held holds court case law cases section right rule order appeal".split())
|
| 22 |
+
def name_tokens(nm):
|
| 23 |
+
return [t for t in re.findall(r"[a-z]+", (nm or "").lower()) if len(t) >= 4 and t not in STOP]
|
| 24 |
+
_BRACKET = re.compile(r"\[[^\]]{0,40}\]") # [Para 23], [1187-C], [122-E- G; 123-8]
|
| 25 |
+
CITE_PAT = re.compile(r"\[?\(?\d{4}\)?\]?(?:\s*\(?\d+\)?){0,2}\s*(?:supp\.?\s*)?(?:scc\s*online\s*sc|s\s?\.?\s?c\s?\.?\s?r\.?|s\s?\.?\s?c\s?\.?\s?c\.?|a\s?\.?\s?i\s?\.?\s?r\.?|insc)\s*\.?(?:\s*\d+)?", re.I)
|
| 26 |
+
|
| 27 |
+
print("loading meta + edges + good_law ...")
|
| 28 |
+
meta = {}
|
| 29 |
+
for l in open(f"{DATA}/escr_meta.jsonl"):
|
| 30 |
+
r = json.loads(l); meta[r["doc_id"]] = r
|
| 31 |
+
edges = [json.loads(l) for l in open(f"{DATA}/edges.jsonl")]
|
| 32 |
+
bad = [json.loads(l) for l in open(f"{DATA}/good_law.jsonl")]
|
| 33 |
+
bad_ids = [g["doc_id"] for g in bad if g.get("good_law_status") in ("overruled", "doubted", "per_incuriam")]
|
| 34 |
+
|
| 35 |
+
queries = [] # (qid, intent, text)
|
| 36 |
+
qrels = [] # (qid, doc_id, grade)
|
| 37 |
+
qid = 0
|
| 38 |
+
|
| 39 |
+
# --- slice 1: citing-passage -> relied-on authority (grade 3) ---
|
| 40 |
+
STRONG = {"relied_on", "followed", "approved", "affirmed"}
|
| 41 |
+
pos = [e for e in edges if e.get("treatment") in STRONG and e.get("para") and e.get("target") in meta]
|
| 42 |
+
random.shuffle(pos)
|
| 43 |
+
n = 0
|
| 44 |
+
for e in pos:
|
| 45 |
+
if n >= 500: break
|
| 46 |
+
tgt = e["target"]; para = e["para"]
|
| 47 |
+
q = _BRACKET.sub(" ", para) # drop [Para..]/[pin] refs
|
| 48 |
+
for t in name_tokens(meta[tgt].get("case_name")): # strip the cited case's distinctive name tokens
|
| 49 |
+
q = re.sub(r"\b" + re.escape(t) + r"\b", " ", q, flags=re.I)
|
| 50 |
+
q = CITE_PAT.sub(" ", q) # strip citations
|
| 51 |
+
q = re.sub(r"\b(?:v|vs|versus)\.?\b", " ", q, flags=re.I) # drop "X v Y" cross-ref connectors
|
| 52 |
+
q = re.sub(r"[^A-Za-z0-9 .,'-]", " ", q)
|
| 53 |
+
q = re.sub(r"\s+", " ", q).strip(" .,-;:")
|
| 54 |
+
words = q.split()
|
| 55 |
+
if len(words) < 12: continue # too short after stripping
|
| 56 |
+
if CITE_PAT.search(q) or re.search(r"\b(scc|scr|air|insc)\b", q, re.I): continue # residual citation -> leaky
|
| 57 |
+
if sum(1 for w in words if w.lower() in FUNC) < 3: continue # must read like prose
|
| 58 |
+
if sum(1 for w in words if w.isupper() and len(w) > 1) > len(words) * 0.3: continue # too many ALLCAPS names
|
| 59 |
+
qid += 1; n += 1
|
| 60 |
+
queries.append((qid, "citing_passage", q))
|
| 61 |
+
qrels.append((qid, tgt, 3))
|
| 62 |
+
n_cite_passage = n
|
| 63 |
+
|
| 64 |
+
# --- slice 2 + 3: known-item lookups (grade 3, single target) ---
|
| 65 |
+
docs = [r for r in meta.values() if r.get("neutral_citation") and r.get("case_name")]
|
| 66 |
+
random.shuffle(docs)
|
| 67 |
+
n_ki_cite = n_ki_name = 0
|
| 68 |
+
for r in docs:
|
| 69 |
+
d = r["doc_id"]
|
| 70 |
+
if n_ki_cite < 150:
|
| 71 |
+
qid += 1; n_ki_cite += 1
|
| 72 |
+
queries.append((qid, "known_item_cite", r["neutral_citation"]))
|
| 73 |
+
qrels.append((qid, d, 3))
|
| 74 |
+
elif n_ki_name < 150:
|
| 75 |
+
nm = re.sub(r"\s*&\s*(anr|ors)\.?", "", r["case_name"], flags=re.I).strip()
|
| 76 |
+
nm = re.sub(r"\s+", " ", nm)
|
| 77 |
+
qid += 1; n_ki_name += 1
|
| 78 |
+
queries.append((qid, "known_item_name", nm))
|
| 79 |
+
qrels.append((qid, d, 3))
|
| 80 |
+
if n_ki_cite >= 150 and n_ki_name >= 150: break
|
| 81 |
+
|
| 82 |
+
# --- write frozen files ---
|
| 83 |
+
with open(f"{OUT}/queries.tsv", "w") as f:
|
| 84 |
+
for q, intent, text in queries: f.write(f"{q}\t{intent}\t{text}\n")
|
| 85 |
+
with open(f"{OUT}/qrels.tsv", "w") as f:
|
| 86 |
+
for q, d, g in qrels: f.write(f"{q}\t{d}\t{g}\n")
|
| 87 |
+
with open(f"{OUT}/bad_law_docids.txt", "w") as f:
|
| 88 |
+
f.write("\n".join(bad_ids) + "\n")
|
| 89 |
+
|
| 90 |
+
print(f"citing_passage : {n_cite_passage}")
|
| 91 |
+
print(f"known_item_cite: {n_ki_cite}")
|
| 92 |
+
print(f"known_item_name: {n_ki_name}")
|
| 93 |
+
print(f"TOTAL queries : {len(queries)} qrels rows: {len(qrels)} bad-law deny-list: {len(bad_ids)}")
|
| 94 |
+
print("--- sample citing_passage queries (name/cite stripped) ---")
|
| 95 |
+
for q, intent, text in [x for x in queries if x[1] == "citing_passage"][:4]:
|
| 96 |
+
print(f" q{q}: {text[:120]}")
|
phase1/eval/classify_intent.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Round-0 intent classifier (the 'router' as a state-setter). Labels each query AUTHORITY
|
| 2 |
+
(wants the leading/landmark case on a doctrine -> apply the authority prior) vs SPECIFIC (a
|
| 3 |
+
particular case / fact-pattern / narrow holding -> no prior). DeepSeek, parallel, cached to JSON.
|
| 4 |
+
|
| 5 |
+
Usage: THEMIS_QFILE=authority_queries.tsv OUT=intent_authority.json python classify_intent.py
|
| 6 |
+
Reads .env for DEEPSEEK_API_KEY. Cached by qid so re-runs are free; delete the OUT file to refresh.
|
| 7 |
+
"""
|
| 8 |
+
import os, json, sys, time, concurrent.futures as cf
|
| 9 |
+
import requests
|
| 10 |
+
|
| 11 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
def _load_env(p):
|
| 13 |
+
if os.path.exists(p):
|
| 14 |
+
for l in open(p):
|
| 15 |
+
l = l.strip()
|
| 16 |
+
if l and not l.startswith("#") and "=" in l:
|
| 17 |
+
k, v = l.split("=", 1); os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
| 18 |
+
_load_env(os.path.join(HERE, "..", "scripts", ".env"))
|
| 19 |
+
KEY = os.environ["DEEPSEEK_API_KEY"]
|
| 20 |
+
HDR = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
|
| 21 |
+
QFILE = os.environ.get("THEMIS_QFILE", "authority_queries.tsv")
|
| 22 |
+
OUT = os.environ.get("OUT", "intent.json")
|
| 23 |
+
|
| 24 |
+
SYS = ("You route a legal search query for an Indian Supreme Court case-law engine. "
|
| 25 |
+
"Decide what the user is after:\n"
|
| 26 |
+
"AUTHORITY = they want the leading / landmark / controlling case(s) on a legal PRINCIPLE, "
|
| 27 |
+
"doctrine, right, or test (e.g. 'is privacy a fundamental right', 'test for sedition', "
|
| 28 |
+
"'doctrine of basic structure').\n"
|
| 29 |
+
"SPECIFIC = they want a particular named case, a narrow fact-pattern match, a specific "
|
| 30 |
+
"statutory provision's application, or a procedural/factual lookup where the single most "
|
| 31 |
+
"authoritative landmark is NOT necessarily the right answer.\n"
|
| 32 |
+
"Reply with EXACTLY one word: AUTHORITY or SPECIFIC.")
|
| 33 |
+
|
| 34 |
+
def classify(text):
|
| 35 |
+
for attempt in range(3):
|
| 36 |
+
try:
|
| 37 |
+
r = requests.post("https://api.deepseek.com/chat/completions", headers=HDR, timeout=40,
|
| 38 |
+
json={"model": "deepseek-chat", "temperature": 0, "max_tokens": 4,
|
| 39 |
+
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": text}]})
|
| 40 |
+
if r.status_code == 200:
|
| 41 |
+
t = r.json()["choices"][0]["message"]["content"].strip().upper()
|
| 42 |
+
return "AUTHORITY" if "AUTHORITY" in t else "SPECIFIC"
|
| 43 |
+
except Exception:
|
| 44 |
+
time.sleep(2 * (attempt + 1))
|
| 45 |
+
return "SPECIFIC" # fail-safe: no prior
|
| 46 |
+
|
| 47 |
+
def main():
|
| 48 |
+
rows = []
|
| 49 |
+
for l in open(QFILE, encoding="utf-8"):
|
| 50 |
+
qid, intent, text = l.rstrip("\n").split("\t", 2); rows.append((qid, text))
|
| 51 |
+
cache = json.load(open(OUT)) if os.path.exists(OUT) else {}
|
| 52 |
+
todo = [(qid, text) for qid, text in rows if qid not in cache]
|
| 53 |
+
print(f"{len(rows)} queries, {len(todo)} to classify ({len(cache)} cached)", flush=True)
|
| 54 |
+
t0 = time.time()
|
| 55 |
+
with cf.ThreadPoolExecutor(max_workers=24) as ex:
|
| 56 |
+
futs = {ex.submit(classify, text): qid for qid, text in todo}
|
| 57 |
+
done = 0
|
| 58 |
+
for f in cf.as_completed(futs):
|
| 59 |
+
cache[futs[f]] = f.result(); done += 1
|
| 60 |
+
if done % 50 == 0:
|
| 61 |
+
json.dump(cache, open(OUT, "w")); print(f" {done}/{len(todo)} {time.time()-t0:.0f}s", flush=True)
|
| 62 |
+
json.dump(cache, open(OUT, "w"))
|
| 63 |
+
n_auth = sum(1 for v in cache.values() if v == "AUTHORITY")
|
| 64 |
+
print(f"done {len(cache)} -> {OUT} | AUTHORITY={n_auth} ({100*n_auth/len(cache):.0f}%) SPECIFIC={len(cache)-n_auth}", flush=True)
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|
phase1/eval/embed_chunks.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Re-embed the corpus chunks with BGE-small on the GPU to regenerate escr_vectors.npy locally
|
| 2 |
+
(faster than transferring the 1.9GB float32 matrix over Thor's slow link). Embeds in FILE ORDER so
|
| 3 |
+
the vector index matches escr_chunks.jsonl line order, exactly as serve.py expects. Documents are
|
| 4 |
+
embedded PLAIN (no query instruction prefix — that's query-side only)."""
|
| 5 |
+
import json, time, os
|
| 6 |
+
import numpy as np
|
| 7 |
+
from sentence_transformers import SentenceTransformer
|
| 8 |
+
DATA = os.environ.get("THEMIS_DATA", ".")
|
| 9 |
+
DEV = os.environ.get("THEMIS_DEVICE", "cuda")
|
| 10 |
+
texts = []
|
| 11 |
+
with open(os.path.join(DATA, "escr_chunks.jsonl"), encoding="utf-8") as f:
|
| 12 |
+
for l in f:
|
| 13 |
+
texts.append(json.loads(l)["text"])
|
| 14 |
+
print(f"{len(texts)} chunks; embedding on {DEV} ...", flush=True)
|
| 15 |
+
st = SentenceTransformer("BAAI/bge-small-en-v1.5", device=DEV)
|
| 16 |
+
t0 = time.time()
|
| 17 |
+
M = st.encode(texts, batch_size=512, normalize_embeddings=True, convert_to_numpy=True,
|
| 18 |
+
show_progress_bar=True).astype(np.float32)
|
| 19 |
+
np.save(os.path.join(DATA, "escr_vectors.npy"), M)
|
| 20 |
+
print(f"done {M.shape} {M.dtype} in {time.time()-t0:.0f}s -> escr_vectors.npy", flush=True)
|
phase1/eval/escr_sample.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
phase1/eval/fetch_feedback.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Pull lawyer feedback bundles from the private HF dataset and digest them.
|
| 3 |
+
|
| 4 |
+
Feedback Mode (the Vercel UI toggle) posts judgment bundles to /api/feedback_bundle,
|
| 5 |
+
which the Space pushes durably to the private dataset vg15o2/themis-feedback
|
| 6 |
+
(one JSON per share under feedback/YYYY-MM-DD/). This script is the owner side:
|
| 7 |
+
download everything, print a digest, dump a flat JSONL, and optionally emit
|
| 8 |
+
qrels-candidate rows for the eval set.
|
| 9 |
+
|
| 10 |
+
Grade map (placement -> graded relevance): top5=3, top10=2, after10=1, irrelevant=0.
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python phase1/eval/fetch_feedback.py # digest + feedback_dump.jsonl
|
| 14 |
+
python phase1/eval/fetch_feedback.py --to-qrels # + feedback_qrels_candidates.tsv
|
| 15 |
+
Needs the HF write token (read suffices) in ~/.git-credentials or HF_TOKEN env.
|
| 16 |
+
"""
|
| 17 |
+
import glob, json, os, socket, subprocess, sys
|
| 18 |
+
|
| 19 |
+
_o = socket.getaddrinfo
|
| 20 |
+
socket.getaddrinfo = lambda h, p, f=0, *a, **k: _o(h, p, socket.AF_INET, *a, **k) # IPv6-first DNS hangs on this box
|
| 21 |
+
|
| 22 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 23 |
+
GRADE = {"top5": 3, "top10": 2, "after10": 1}
|
| 24 |
+
|
| 25 |
+
def token():
|
| 26 |
+
t = os.environ.get("HF_TOKEN")
|
| 27 |
+
if t: return t
|
| 28 |
+
return subprocess.run(["bash", "-c",
|
| 29 |
+
"grep -m1 'huggingface.co' ~/.git-credentials | sed -E 's#https://[^:]*:([^@]+)@.*#\\1#'"],
|
| 30 |
+
capture_output=True, text=True).stdout.strip()
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
from huggingface_hub import snapshot_download
|
| 34 |
+
local = snapshot_download("vg15o2/themis-feedback", repo_type="dataset", token=token())
|
| 35 |
+
files = sorted(glob.glob(os.path.join(local, "feedback", "**", "*.json"), recursive=True))
|
| 36 |
+
bundles = []
|
| 37 |
+
for f in files: # daily arrays (current) or single objects (legacy)
|
| 38 |
+
data = json.load(open(f, encoding="utf-8"))
|
| 39 |
+
bundles.extend(data if isinstance(data, list) else [data])
|
| 40 |
+
print(f"[feedback] {len(bundles)} bundle(s) across {len(files)} file(s)\n")
|
| 41 |
+
|
| 42 |
+
dump = os.path.join(HERE, "feedback_dump.jsonl")
|
| 43 |
+
with open(dump, "w", encoding="utf-8") as f:
|
| 44 |
+
for b in bundles: f.write(json.dumps(b, ensure_ascii=False) + "\n")
|
| 45 |
+
|
| 46 |
+
for b in bundles:
|
| 47 |
+
judged = [r for r in b.get("results", []) if r.get("placement") or r.get("irrelevant") or (r.get("comment") or "").strip()]
|
| 48 |
+
print(f"— {b.get('server_ts', b.get('ts',''))[:16]} {b.get('name','?')} [{b.get('mode','auto')}]")
|
| 49 |
+
print(f" Q: {b.get('q','')[:100]}")
|
| 50 |
+
for r in judged:
|
| 51 |
+
mark = "IRRELEVANT" if r.get("irrelevant") else (r.get("placement") or "")
|
| 52 |
+
c = (r.get("comment") or "").strip()
|
| 53 |
+
print(f" #{r.get('rank_shown','?'):>3} {mark:<10} {r.get('case_name','')[:52]}" + (f' "{c[:70]}"' if c else ""))
|
| 54 |
+
if (b.get("missing_case") or "").strip(): print(f" MISSING: {b['missing_case'][:100]}")
|
| 55 |
+
if (b.get("additional") or "").strip(): print(f" NOTE: {b['additional'][:140]}")
|
| 56 |
+
print()
|
| 57 |
+
print(f"[feedback] dump -> {dump}")
|
| 58 |
+
|
| 59 |
+
if "--to-qrels" in sys.argv:
|
| 60 |
+
out = os.path.join(HERE, "feedback_qrels_candidates.tsv")
|
| 61 |
+
with open(out, "w", encoding="utf-8") as f:
|
| 62 |
+
f.write("# qid(query text)\tdoc_id\tgrade\tsource\n")
|
| 63 |
+
for b in bundles:
|
| 64 |
+
for r in b.get("results", []):
|
| 65 |
+
if r.get("irrelevant"): g = 0
|
| 66 |
+
elif r.get("placement") in GRADE: g = GRADE[r["placement"]]
|
| 67 |
+
else: continue
|
| 68 |
+
f.write(f"{b.get('q','')}\t{r['doc_id']}\t{g}\t{b.get('name','?')}\n")
|
| 69 |
+
print(f"[feedback] qrels candidates -> {out} (review before merging into the eval set)")
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
main()
|
phase1/eval/gen_sample.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
phase1/eval/gold_foundational.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"fact-1","query":"husband and his family harassing wife for dowry, can the FIR under section 498A be quashed if the parties reach a settlement","control":false,"foundational":[["gian","singh","punjab"],["jitendra","raghuvanshi"]]},
|
| 3 |
+
{"id":"fact-2","query":"cheque issued on behalf of a company was dishonoured, is the director personally liable under section 138 of the Negotiable Instruments Act","control":false,"foundational":[["aneeta","hada"],["pharmaceuticals","neeta"]]},
|
| 4 |
+
{"id":"fact-3","query":"employee dismissed from service without a departmental inquiry, whether the termination violates principles of natural justice","control":false,"foundational":[["tulsiram","patel"],["maneka","gandhi"]]},
|
| 5 |
+
{"id":"fact-4","query":"accused seeking anticipatory bail in an economic offence involving diversion of investor money","control":false,"foundational":[["chidambaram"],["sushila","aggarwal"]]},
|
| 6 |
+
{"id":"fact-5","query":"government acquired private land and the owner claims the compensation awarded is far below the market value","control":false,"foundational":[]},
|
| 7 |
+
{"id":"fact-6","query":"person contracted a second marriage while the divorce petition from the first marriage was still pending, validity and bigamy","control":false,"foundational":[["sarla","mudgal"]]},
|
| 8 |
+
{"id":"issue-1","query":"whether a dying declaration alone, without corroboration, is sufficient to sustain a conviction","control":false,"foundational":[["khushal","rao"]]},
|
| 9 |
+
{"id":"issue-2","query":"the tests for grant of a temporary injunction: prima facie case, balance of convenience and irreparable injury","control":false,"foundational":[["dalpat","kumar"],["gujarat","bottling"]]},
|
| 10 |
+
{"id":"issue-3","query":"scope of judicial review of administrative action on the ground of arbitrariness under Article 14","control":false,"foundational":[["shrilekha","vidyarthi"],["royappa"]]},
|
| 11 |
+
{"id":"issue-4","query":"whether bail once granted can be cancelled merely on the basis of subsequent developments","control":false,"foundational":[["dolat","ram"]]},
|
| 12 |
+
{"id":"vague-1","query":"the supreme court judgment holding that privacy is a fundamental right, connected with the aadhaar matter","control":false,"foundational":[["puttaswamy"]]},
|
| 13 |
+
{"id":"vague-2","query":"constitution bench decision on reservation in promotion for scheduled caste and scheduled tribe employees","control":false,"foundational":[["indra","sawhney"],["jarnail","singh"]]},
|
| 14 |
+
{"id":"citation-1","query":"(2017) 10 SCC 1","control":true,"foundational":[["puttaswamy"]]},
|
| 15 |
+
{"id":"casename-1","query":"K.S. Puttaswamy v Union of India","control":true,"foundational":[["puttaswamy"]]},
|
| 16 |
+
{"id":"casename-2","query":"Vishaka v State of Rajasthan","control":true,"foundational":[["vishaka","rajasthan"]]}
|
| 17 |
+
]
|