harshrawat18 commited on
Commit
99cc0f4
Β·
1 Parent(s): 70bc888

feat: sprint 17 - linguistic hegemony & vector synchronization

Browse files
Dockerfile CHANGED
@@ -1,28 +1,59 @@
 
 
 
 
 
 
1
  FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- # Layer 1: Install system dependencies
6
- RUN apt-get update && apt-get install -y \
 
 
 
7
  build-essential \
8
  git \
 
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
- # Layer 2: Install Python dependencies
 
 
 
 
 
 
 
 
12
  COPY requirements.txt .
13
  RUN pip install --no-cache-dir -r requirements.txt
14
 
15
- # Layer 3: Pre-cache the EXACT models used in api.py at build time
16
- # This prevents runtime downloads and eliminates cold-start latency
 
17
  RUN python -c "\
18
  from sentence_transformers import SentenceTransformer, CrossEncoder; \
19
  SentenceTransformer('nomic-ai/nomic-embed-text-v1', cache_folder='/app/models', trust_remote_code=True); \
20
- print('βœ“ Nomic Embedding model cached'); \
21
  CrossEncoder('cross-encoder/ettin-reranker-68m-v1', max_length=512); \
22
  print('βœ“ Ettin Reranker cached')"
23
 
24
- # Layer 4: Copy application code last
25
- # This ensures code changes don't invalidate the model cache layer
 
 
 
 
 
 
 
 
 
 
 
 
26
  COPY . .
27
 
28
  EXPOSE 7860
 
1
+ # ═══════════════════════════════════════════════════════════════
2
+ # GovBridge India β€” Production Dockerfile
3
+ # Target: Hugging Face Spaces (CPU Basic: 2 vCPU, 16GB RAM)
4
+ # Constraint: ZERO CUDA binaries. CPU-only PyTorch wheels.
5
+ # ═══════════════════════════════════════════════════════════════
6
+
7
  FROM python:3.11-slim
8
 
9
  WORKDIR /app
10
 
11
+ # ── Layer 1: OS Dependencies (ROOT) ──────────────────────────
12
+ # libgomp1: Required for OpenMP multi-threading (IndicTrans2 + PyTorch CPU)
13
+ # build-essential: Required for Cython compilation (IndicTransToolkit)
14
+ # git: Required for pip install from GitHub repos
15
+ RUN apt-get update && apt-get install -y --no-install-recommends \
16
  build-essential \
17
  git \
18
+ libgomp1 \
19
  && rm -rf /var/lib/apt/lists/*
20
 
21
+ # ── Layer 2: CPU-Only PyTorch Installation ───────────────────
22
+ # CRITICAL: This MUST run BEFORE requirements.txt to prevent
23
+ # pip from resolving torch from the default PyPI index (which
24
+ # downloads 1.2GB+ of CUDA/NVIDIA binaries and exhausts disk).
25
+ RUN pip install --no-cache-dir \
26
+ torch>=2.2.0 \
27
+ --extra-index-url https://download.pytorch.org/whl/cpu
28
+
29
+ # ── Layer 3: Python Dependencies ─────────────────────────────
30
  COPY requirements.txt .
31
  RUN pip install --no-cache-dir -r requirements.txt
32
 
33
+ # ── Layer 4: Pre-cache ALL ML models at build time ───────────
34
+ # This eliminates cold-start latency on first request.
35
+ # Models are cached to /app/models (persisted across container restarts).
36
  RUN python -c "\
37
  from sentence_transformers import SentenceTransformer, CrossEncoder; \
38
  SentenceTransformer('nomic-ai/nomic-embed-text-v1', cache_folder='/app/models', trust_remote_code=True); \
39
+ print('βœ“ Nomic Embedding model (768-dim) cached'); \
40
  CrossEncoder('cross-encoder/ettin-reranker-68m-v1', max_length=512); \
41
  print('βœ“ Ettin Reranker cached')"
42
 
43
+ # ── Layer 5: Pre-cache IndicTrans2 translation model ─────────
44
+ # Cached separately because it requires trust_remote_code and
45
+ # downloads ~800MB of weights. Failure here must NOT block deployment.
46
+ RUN python -c "\
47
+ try: \
48
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM; \
49
+ AutoTokenizer.from_pretrained('ai4bharat/indictrans2-en-indic-dist-200M', trust_remote_code=True, cache_dir='/app/models/indictrans2'); \
50
+ AutoModelForSeq2SeqLM.from_pretrained('ai4bharat/indictrans2-en-indic-dist-200M', trust_remote_code=True, cache_dir='/app/models/indictrans2'); \
51
+ print('βœ“ IndicTrans2 translation model cached'); \
52
+ except Exception as e: \
53
+ print(f'⚠️ IndicTrans2 cache failed (non-blocking): {e}')"
54
+
55
+ # ── Layer 6: Copy application code LAST ──────────────────────
56
+ # Code changes don't invalidate the expensive model cache layers.
57
  COPY . .
58
 
59
  EXPOSE 7860
__pycache__/ai_brain.cpython-312.pyc DELETED
Binary file (1.63 kB)
 
__pycache__/api.cpython-312.pyc DELETED
Binary file (19.4 kB)
 
__pycache__/bhashini.cpython-312.pyc DELETED
Binary file (7.2 kB)
 
__pycache__/config.cpython-312.pyc DELETED
Binary file (810 Bytes)
 
__pycache__/scraper.cpython-312.pyc DELETED
Binary file (9.94 kB)
 
api.py CHANGED
@@ -20,9 +20,9 @@ from config import settings
20
 
21
  # --- SECURE KEYS ---
22
  SUPABASE_URL = settings.SUPABASE_URL
23
- SUPABASE_KEY = settings.SUPABASE_KEY
24
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
25
- ADMIN_SECRET = os.environ.get("ADMIN_SECRET", "change-this-in-production")
26
 
27
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
28
  groq_client = Groq(api_key=GROQ_API_KEY)
@@ -197,8 +197,9 @@ async def ingest_document(request: IngestRequest, admin_key: str = ""):
197
  prefixed_chunks = [f"search_document: {c}" for c in chunks]
198
  embeddings = model.encode(prefixed_chunks, normalize_embeddings=True, batch_size=32, show_progress_bar=False).tolist()
199
  rows = []
 
200
  for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
201
- rows.append({
202
  "chunk_index": i,
203
  "chunk_text": chunk,
204
  "scheme_title": request.title,
@@ -208,7 +209,11 @@ async def ingest_document(request: IngestRequest, admin_key: str = ""):
208
  "doc_type": request.doc_type,
209
  "embedding": embedding,
210
  "content_hash": doc_hash
211
- })
 
 
 
 
212
  supabase.table("document_chunks").upsert(rows, on_conflict="content_hash,chunk_index").execute()
213
  return {"status": "success", "title": request.title, "chunks_created": len(chunks), "doc_hash": doc_hash}
214
 
 
20
 
21
  # --- SECURE KEYS ---
22
  SUPABASE_URL = settings.SUPABASE_URL
23
+ SUPABASE_KEY = settings.SUPABASE_KEY
24
+ GROQ_API_KEY = settings.GROQ_API_KEY
25
+ ADMIN_SECRET = settings.ADMIN_SECRET
26
 
27
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
28
  groq_client = Groq(api_key=GROQ_API_KEY)
 
197
  prefixed_chunks = [f"search_document: {c}" for c in chunks]
198
  embeddings = model.encode(prefixed_chunks, normalize_embeddings=True, batch_size=32, show_progress_bar=False).tolist()
199
  rows = []
200
+ dual_write = settings.DUAL_WRITE_EMBEDDINGS.lower() == "true"
201
  for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
202
+ row = {
203
  "chunk_index": i,
204
  "chunk_text": chunk,
205
  "scheme_title": request.title,
 
209
  "doc_type": request.doc_type,
210
  "embedding": embedding,
211
  "content_hash": doc_hash
212
+ }
213
+ # During expand-contract migration: write to BOTH columns
214
+ if dual_write:
215
+ row["embedding_v2"] = embedding
216
+ rows.append(row)
217
  supabase.table("document_chunks").upsert(rows, on_conflict="content_hash,chunk_index").execute()
218
  return {"status": "success", "title": request.title, "chunks_created": len(chunks), "doc_hash": doc_hash}
219
 
bhashini.py CHANGED
@@ -88,7 +88,7 @@ def local_indictrans_translate(text: str, src: str, tgt: str) -> str:
88
  return_attention_mask=True
89
  )
90
 
91
- with torch.no_grad():
92
  generated_tokens = _indictrans_model.generate(
93
  **inputs,
94
  num_beams=4,
@@ -120,7 +120,7 @@ def local_indictrans_translate(text: str, src: str, tgt: str) -> str:
120
  truncation=True,
121
  max_length=512
122
  )
123
- with torch.no_grad():
124
  outputs = _indictrans_model.generate(
125
  **inputs,
126
  tgt_lang=tgt_code,
 
88
  return_attention_mask=True
89
  )
90
 
91
+ with torch.inference_mode():
92
  generated_tokens = _indictrans_model.generate(
93
  **inputs,
94
  num_beams=4,
 
120
  truncation=True,
121
  max_length=512
122
  )
123
+ with torch.inference_mode():
124
  outputs = _indictrans_model.generate(
125
  **inputs,
126
  tgt_lang=tgt_code,
config.py CHANGED
@@ -1,12 +1,48 @@
 
 
 
 
 
1
  from pydantic_settings import BaseSettings, SettingsConfigDict
2
  from pydantic import Field
3
  from typing import Optional
4
- import os
5
 
6
  class Settings(BaseSettings):
 
7
  SUPABASE_URL: str = Field(default="")
8
  SUPABASE_KEY: str = Field(default="")
9
 
10
- model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  settings = Settings()
 
1
+ """
2
+ GovBridge India β€” Centralized Configuration
3
+ All environment variables are declared here with Pydantic validation.
4
+ Usage: from config import settings
5
+ """
6
  from pydantic_settings import BaseSettings, SettingsConfigDict
7
  from pydantic import Field
8
  from typing import Optional
9
+
10
 
11
  class Settings(BaseSettings):
12
+ # ── Database ─────────────────────────────────────────
13
  SUPABASE_URL: str = Field(default="")
14
  SUPABASE_KEY: str = Field(default="")
15
 
16
+ # ── LLM ──────────────────────────────────────────────
17
+ GROQ_API_KEY: str = Field(default="")
18
+
19
+ # ── Admin ────────────────────────────────────────────
20
+ ADMIN_SECRET: str = Field(default="change-this-in-production")
21
+
22
+ # ── Translation (Bhashini) ───────────────────────────
23
+ BHASHINI_USER_ID: Optional[str] = Field(default=None)
24
+ BHASHINI_API_KEY: Optional[str] = Field(default=None)
25
+
26
+ # ── WhatsApp ─────────────────────────────────────────
27
+ WHATSAPP_TOKEN: Optional[str] = Field(default=None)
28
+ WHATSAPP_PHONE_NUMBER_ID: Optional[str] = Field(default=None)
29
+ WHATSAPP_VERIFY_TOKEN: Optional[str] = Field(default=None)
30
+ WHATSAPP_APP_SECRET: Optional[str] = Field(default=None)
31
+
32
+ # ── Feature Flags ────────────────────────────────────
33
+ ENVIRONMENT: str = Field(default="development")
34
+
35
+ # ── Migration Flag ───────────────────────────────────
36
+ # Set to "true" during the expand-contract migration window.
37
+ # When true, ingest writes to BOTH embedding AND embedding_v2.
38
+ # After migration 009 is complete, set back to "false".
39
+ DUAL_WRITE_EMBEDDINGS: str = Field(default="false")
40
+
41
+ model_config = SettingsConfigDict(
42
+ env_file=".env",
43
+ env_file_encoding="utf-8",
44
+ extra="ignore"
45
+ )
46
+
47
 
48
  settings = Settings()
requirements.txt CHANGED
@@ -1,22 +1,43 @@
 
 
 
 
 
 
 
1
  fastapi
2
  uvicorn[standard]
3
- torch>=2.2.0
 
 
 
4
  sentence-transformers>=2.7.0
5
  einops
6
  huggingface-hub
 
 
 
 
 
 
 
 
 
 
 
7
  groq
 
 
8
  supabase
 
 
9
  slowapi
10
- openfisca-core
11
- transformers>=4.48.0
12
- sentencepiece>=0.2.0
13
- sacremoses>=0.1.1
14
  httpx
 
 
15
  feedparser
16
- IndicTransToolkit
17
- onnx
18
- onnxruntime
19
- optimum
20
- pydantic-settings
21
  tenacity
22
  pydantic
 
 
1
+ # ═══════════════════════════════════════════
2
+ # GovBridge India β€” Python Dependencies
3
+ # Target: Hugging Face Spaces (CPU Basic: 2 vCPU, 16GB RAM)
4
+ # CRITICAL: All ML wheels MUST be CPU-only to prevent disk exhaustion
5
+ # ═══════════════════════════════════════════
6
+
7
+ # --- Core Web Framework ---
8
  fastapi
9
  uvicorn[standard]
10
+
11
+ # --- ML / AI (CPU-ONLY WHEELS) ---
12
+ # CRITICAL: --extra-index-url in Dockerfile forces CPU torch
13
+ # DO NOT add torch here β€” it is installed separately in Dockerfile Layer 2
14
  sentence-transformers>=2.7.0
15
  einops
16
  huggingface-hub
17
+
18
+ # --- Translation Pipeline ---
19
+ # CRITICAL PIN: transformers==4.40.2 is the exact temporal anchor
20
+ # that supports IndicTrans2 WITHOUT the ONNX removal crash.
21
+ # Versions >=4.41.0 removed transformers.onnx, causing ImportError.
22
+ transformers==4.40.2
23
+ sentencepiece>=0.2.0
24
+ sacremoses>=0.1.1
25
+ IndicTransToolkit
26
+
27
+ # --- LLM Inference ---
28
  groq
29
+
30
+ # --- Database ---
31
  supabase
32
+
33
+ # --- Rate Limiting (in-process, for local dev) ---
34
  slowapi
35
+
36
+ # --- HTTP Client ---
 
 
37
  httpx
38
+
39
+ # --- Data Processing ---
40
  feedparser
 
 
 
 
 
41
  tenacity
42
  pydantic
43
+ pydantic-settings
scripts/backfill_768.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GovBridge India β€” Phase 2: Backfill 768-dim Embeddings
3
+ Sprint 17: Zero-Downtime Expand-Contract Migration
4
+
5
+ PREREQUISITES:
6
+ 1. Migration 007 has been executed (embedding_v2 column exists)
7
+ 2. Nomic model is available (pip install sentence-transformers einops)
8
+
9
+ USAGE:
10
+ cd /workspaces/govbridge
11
+ python3 gov_backend/scripts/backfill_768.py
12
+
13
+ BEHAVIOR:
14
+ - Reads ALL rows from document_chunks where embedding_v2 IS NULL
15
+ - Generates 768-dim embeddings using nomic-embed-text-v1
16
+ - UPSERTs in batches of 50 rows using keyset pagination
17
+ - Also backfills query_cache.query_embedding_v2
18
+ - Safe to re-run (idempotent via IS NULL filter)
19
+ """
20
+ import os
21
+ import sys
22
+ import time
23
+ import torch
24
+
25
+ # Ensure gov_backend is on the Python path
26
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+ from config import settings
28
+
29
+ from supabase import create_client
30
+ from sentence_transformers import SentenceTransformer
31
+
32
+ # ── Configuration ────────────────────────────────────────────
33
+ BATCH_SIZE = 50 # Rows per batch (free-tier Supabase safe)
34
+ SLEEP_BETWEEN = 0.5 # Seconds between batches (prevent rate limit)
35
+ MAX_ROWS = 5000 # Safety ceiling per run
36
+
37
+ def main():
38
+ supabase_url = settings.SUPABASE_URL
39
+ supabase_key = settings.SUPABASE_KEY
40
+
41
+ if not supabase_url or not supabase_key:
42
+ print("❌ FATAL: Missing SUPABASE_URL or SUPABASE_KEY in environment")
43
+ sys.exit(1)
44
+
45
+ print("⏳ Loading Nomic Embedding Model (768-dim)...")
46
+ model = SentenceTransformer(
47
+ 'nomic-ai/nomic-embed-text-v1',
48
+ trust_remote_code=True
49
+ )
50
+ print("βœ… Nomic model loaded")
51
+
52
+ supabase = create_client(supabase_url, supabase_key)
53
+
54
+ # ── Phase 2A: Backfill document_chunks.embedding_v2 ──────
55
+ print("\n═══ PHASE 2A: Backfilling document_chunks.embedding_v2 ═══")
56
+
57
+ result = supabase.table('document_chunks') \
58
+ .select('id, chunk_text, scheme_title') \
59
+ .is_('embedding_v2', 'null') \
60
+ .limit(MAX_ROWS) \
61
+ .execute()
62
+
63
+ chunks = result.data or []
64
+ total = len(chunks)
65
+
66
+ if total == 0:
67
+ print("πŸŽ‰ document_chunks: All rows already have embedding_v2")
68
+ else:
69
+ print(f"Found {total} chunks to backfill")
70
+
71
+ for i in range(0, total, BATCH_SIZE):
72
+ batch = chunks[i:i + BATCH_SIZE]
73
+ batch_num = (i // BATCH_SIZE) + 1
74
+ total_batches = (total + BATCH_SIZE - 1) // BATCH_SIZE
75
+
76
+ # Nomic requires "search_document:" prefix for document embeddings
77
+ texts = [
78
+ f"search_document: {c.get('chunk_text', '') or c.get('scheme_title', '')}"
79
+ for c in batch
80
+ ]
81
+
82
+ print(f"πŸ”„ Batch {batch_num}/{total_batches} ({len(batch)} rows)...")
83
+
84
+ with torch.inference_mode():
85
+ embeddings = model.encode(
86
+ texts,
87
+ normalize_embeddings=True,
88
+ show_progress_bar=False,
89
+ batch_size=32
90
+ ).tolist()
91
+
92
+ # UPSERT each row's embedding_v2
93
+ for chunk, embedding in zip(batch, embeddings):
94
+ supabase.table('document_chunks') \
95
+ .update({'embedding_v2': embedding}) \
96
+ .eq('id', chunk['id']) \
97
+ .execute()
98
+
99
+ print(f"βœ… Batch {batch_num}/{total_batches} complete")
100
+ time.sleep(SLEEP_BETWEEN)
101
+
102
+ print(f"\nπŸŽ‰ document_chunks backfill complete: {total} rows updated")
103
+
104
+ # ── Phase 2B: Backfill query_cache.query_embedding_v2 ────
105
+ print("\n═══ PHASE 2B: Backfilling query_cache.query_embedding_v2 ═══")
106
+
107
+ cache_result = supabase.table('query_cache') \
108
+ .select('id, query_text') \
109
+ .is_('query_embedding_v2', 'null') \
110
+ .limit(MAX_ROWS) \
111
+ .execute()
112
+
113
+ cache_rows = cache_result.data or []
114
+ cache_total = len(cache_rows)
115
+
116
+ if cache_total == 0:
117
+ print("πŸŽ‰ query_cache: All rows already have query_embedding_v2")
118
+ else:
119
+ print(f"Found {cache_total} cache entries to backfill")
120
+
121
+ for i in range(0, cache_total, BATCH_SIZE):
122
+ batch = cache_rows[i:i + BATCH_SIZE]
123
+ batch_num = (i // BATCH_SIZE) + 1
124
+
125
+ # Nomic requires "search_query:" prefix for query embeddings
126
+ texts = [
127
+ f"search_query: {c.get('query_text', '')}"
128
+ for c in batch
129
+ ]
130
+
131
+ with torch.inference_mode():
132
+ embeddings = model.encode(
133
+ texts,
134
+ normalize_embeddings=True,
135
+ show_progress_bar=False,
136
+ batch_size=32
137
+ ).tolist()
138
+
139
+ for row, embedding in zip(batch, embeddings):
140
+ supabase.table('query_cache') \
141
+ .update({'query_embedding_v2': embedding}) \
142
+ .eq('id', row['id']) \
143
+ .execute()
144
+
145
+ time.sleep(SLEEP_BETWEEN)
146
+
147
+ print(f"\nπŸŽ‰ query_cache backfill complete: {cache_total} rows updated")
148
+
149
+ # ── Summary ──────────────────────────────────────────────
150
+ print("\n" + "═" * 60)
151
+ print("MIGRATION PHASE 2 COMPLETE")
152
+ print(f" document_chunks: {total} rows backfilled")
153
+ print(f" query_cache: {cache_total} rows backfilled")
154
+ print("═" * 60)
155
+ print("\nNEXT STEPS:")
156
+ print(" 1. Run migration 008 (concurrent index creation)")
157
+ print(" 2. Verify indexes are VALID")
158
+ print(" 3. Run migration 009 (contract β€” column swap)")
159
+
160
+
161
+ if __name__ == '__main__':
162
+ main()