Spaces:
Sleeping
Sleeping
feat: unified SSOT architecture and scaled vector database ingestion
Browse files- ingestion/__pycache__/ocr_processor.cpython-312.pyc +0 -0
- ingestion/ocr_processor_runner.py +23 -23
- scraper.py +65 -53
ingestion/__pycache__/ocr_processor.cpython-312.pyc
ADDED
|
Binary file (2.64 kB). View file
|
|
|
ingestion/ocr_processor_runner.py
CHANGED
|
@@ -2,19 +2,15 @@ import os
|
|
| 2 |
import sys
|
| 3 |
import hashlib
|
| 4 |
import httpx
|
| 5 |
-
from supabase import create_client
|
| 6 |
|
| 7 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 8 |
from ingestion.ocr_processor import download_and_extract
|
| 9 |
|
| 10 |
-
|
| 11 |
-
SUPABASE_KEY = os.environ["SUPABASE_KEY"]
|
| 12 |
HF_SPACE_URL = os.environ.get("HF_SPACE_URL", "https://harshrawat18-govbridge-api.hf.space")
|
| 13 |
ADMIN_SECRET = os.environ.get("ADMIN_SECRET", "")
|
| 14 |
PDF_URL = os.environ.get("PDF_URL", "")
|
| 15 |
|
| 16 |
-
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 17 |
-
|
| 18 |
def chunk_text(text: str, chunk_size: int = 300) -> list:
|
| 19 |
"""Split text into word-based chunks with overlap."""
|
| 20 |
words = text.split()
|
|
@@ -27,8 +23,8 @@ def chunk_text(text: str, chunk_size: int = 300) -> list:
|
|
| 27 |
chunks.append(chunk)
|
| 28 |
return chunks
|
| 29 |
|
| 30 |
-
def
|
| 31 |
-
"""Send chunks to the
|
| 32 |
for i, chunk in enumerate(chunks):
|
| 33 |
content_hash = hashlib.sha256(
|
| 34 |
f"{metadata['source_url']}_{i}".encode()
|
|
@@ -39,8 +35,8 @@ def ingest_to_supabase(chunks: list, metadata: dict):
|
|
| 39 |
"text": chunk,
|
| 40 |
"source_url": metadata["source_url"],
|
| 41 |
"ministry": metadata.get("ministry", "Government of India"),
|
| 42 |
-
"state": metadata.get("state", "
|
| 43 |
-
"doc_type": metadata.get("doc_type", "
|
| 44 |
"content_hash": content_hash,
|
| 45 |
"chunk_index": i
|
| 46 |
}
|
|
@@ -54,7 +50,7 @@ def ingest_to_supabase(chunks: list, metadata: dict):
|
|
| 54 |
timeout=30.0
|
| 55 |
)
|
| 56 |
if response.status_code == 200:
|
| 57 |
-
print(f"β
Chunk {i+1}/{len(chunks)}
|
| 58 |
else:
|
| 59 |
print(f"β οΈ Chunk {i+1} failed: {response.status_code} {response.text}")
|
| 60 |
except Exception as e:
|
|
@@ -62,34 +58,38 @@ def ingest_to_supabase(chunks: list, metadata: dict):
|
|
| 62 |
|
| 63 |
def main():
|
| 64 |
if not PDF_URL:
|
| 65 |
-
print("β No PDF_URL provided")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
sys.exit(1)
|
| 67 |
|
| 68 |
-
print(f"π Starting PDF extraction: {PDF_URL}")
|
| 69 |
|
| 70 |
try:
|
| 71 |
result = download_and_extract(PDF_URL)
|
| 72 |
text = result["text"]
|
| 73 |
word_count = len(text.split())
|
| 74 |
-
print(f"β
Extracted {word_count} words via {result['extraction_method']}")
|
| 75 |
-
|
| 76 |
-
|
| 77 |
chunks = chunk_text(text, chunk_size=300)
|
| 78 |
-
print(f"π¦
|
| 79 |
|
| 80 |
-
|
|
|
|
| 81 |
"source_url": PDF_URL,
|
| 82 |
-
"title": "
|
| 83 |
-
"ministry": "
|
| 84 |
-
"state": "
|
| 85 |
-
"doc_type": "
|
| 86 |
})
|
| 87 |
|
| 88 |
-
print(f"π
|
| 89 |
|
| 90 |
except Exception as e:
|
| 91 |
print(f"β Critical failure: {e}")
|
| 92 |
sys.exit(1)
|
| 93 |
|
| 94 |
if __name__ == "__main__":
|
| 95 |
-
main()
|
|
|
|
| 2 |
import sys
|
| 3 |
import hashlib
|
| 4 |
import httpx
|
|
|
|
| 5 |
|
| 6 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 7 |
from ingestion.ocr_processor import download_and_extract
|
| 8 |
|
| 9 |
+
# --- SSOT INGESTION CONFIGURATION ---
|
|
|
|
| 10 |
HF_SPACE_URL = os.environ.get("HF_SPACE_URL", "https://harshrawat18-govbridge-api.hf.space")
|
| 11 |
ADMIN_SECRET = os.environ.get("ADMIN_SECRET", "")
|
| 12 |
PDF_URL = os.environ.get("PDF_URL", "")
|
| 13 |
|
|
|
|
|
|
|
| 14 |
def chunk_text(text: str, chunk_size: int = 300) -> list:
|
| 15 |
"""Split text into word-based chunks with overlap."""
|
| 16 |
words = text.split()
|
|
|
|
| 23 |
chunks.append(chunk)
|
| 24 |
return chunks
|
| 25 |
|
| 26 |
+
def ingest_to_hf(chunks: list, metadata: dict):
|
| 27 |
+
"""Send chunks straight to the AI Vector Database (HF Space)."""
|
| 28 |
for i, chunk in enumerate(chunks):
|
| 29 |
content_hash = hashlib.sha256(
|
| 30 |
f"{metadata['source_url']}_{i}".encode()
|
|
|
|
| 35 |
"text": chunk,
|
| 36 |
"source_url": metadata["source_url"],
|
| 37 |
"ministry": metadata.get("ministry", "Government of India"),
|
| 38 |
+
"state": metadata.get("state", "National"),
|
| 39 |
+
"doc_type": metadata.get("doc_type", "document"),
|
| 40 |
"content_hash": content_hash,
|
| 41 |
"chunk_index": i
|
| 42 |
}
|
|
|
|
| 50 |
timeout=30.0
|
| 51 |
)
|
| 52 |
if response.status_code == 200:
|
| 53 |
+
print(f"β
Chunk {i+1}/{len(chunks)} injected into AI Brain")
|
| 54 |
else:
|
| 55 |
print(f"β οΈ Chunk {i+1} failed: {response.status_code} {response.text}")
|
| 56 |
except Exception as e:
|
|
|
|
| 58 |
|
| 59 |
def main():
|
| 60 |
if not PDF_URL:
|
| 61 |
+
print("β No PDF_URL provided in terminal")
|
| 62 |
+
sys.exit(1)
|
| 63 |
+
|
| 64 |
+
if not ADMIN_SECRET:
|
| 65 |
+
print("β π SECURITY HALT: ADMIN_SECRET environment variable is missing!")
|
| 66 |
sys.exit(1)
|
| 67 |
|
| 68 |
+
print(f"π Starting PDF memory extraction: {PDF_URL}")
|
| 69 |
|
| 70 |
try:
|
| 71 |
result = download_and_extract(PDF_URL)
|
| 72 |
text = result["text"]
|
| 73 |
word_count = len(text.split())
|
| 74 |
+
print(f"β
Extracted {word_count} words in memory via {result['extraction_method']}")
|
| 75 |
+
|
|
|
|
| 76 |
chunks = chunk_text(text, chunk_size=300)
|
| 77 |
+
print(f"π¦ Sliced into {len(chunks)} high-density AI chunks")
|
| 78 |
|
| 79 |
+
# Dynamically injecting NEP Metadata for this specific run
|
| 80 |
+
ingest_to_hf(chunks, {
|
| 81 |
"source_url": PDF_URL,
|
| 82 |
+
"title": "National Education Policy 2020",
|
| 83 |
+
"ministry": "Ministry of Education",
|
| 84 |
+
"state": "National",
|
| 85 |
+
"doc_type": "document"
|
| 86 |
})
|
| 87 |
|
| 88 |
+
print(f"π MASSIVE W! {len(chunks)} chunks ingested to Vector DB.")
|
| 89 |
|
| 90 |
except Exception as e:
|
| 91 |
print(f"β Critical failure: {e}")
|
| 92 |
sys.exit(1)
|
| 93 |
|
| 94 |
if __name__ == "__main__":
|
| 95 |
+
main()
|
scraper.py
CHANGED
|
@@ -2,20 +2,17 @@ import os
|
|
| 2 |
import asyncio
|
| 3 |
import hashlib
|
| 4 |
import random
|
|
|
|
| 5 |
from playwright.async_api import async_playwright
|
| 6 |
-
from supabase import create_client, Client
|
| 7 |
import logging
|
| 8 |
|
| 9 |
# Setup logging
|
| 10 |
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
| 11 |
logger = logging.getLogger(__name__)
|
| 12 |
|
| 13 |
-
# ---
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
# Connect to the database
|
| 18 |
-
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 19 |
|
| 20 |
# --- STEALTH SCRIPT COLLECTION ---
|
| 21 |
STEALTH_SCRIPTS = [
|
|
@@ -148,26 +145,56 @@ GOVERNMENT_SITES = [
|
|
| 148 |
"url": "https://www.myscheme.gov.in/search",
|
| 149 |
"source": "MyScheme Portal",
|
| 150 |
"selector": "h2",
|
| 151 |
-
"extract_type": "myscheme"
|
|
|
|
| 152 |
},
|
| 153 |
{
|
| 154 |
"name": "PM-KISAN Scheme",
|
| 155 |
"url": "https://pmkisan.gov.in/",
|
| 156 |
"source": "Ministry of Agriculture",
|
| 157 |
"selector": "h2, h3, div.content",
|
| 158 |
-
"extract_type": "generic"
|
|
|
|
| 159 |
},
|
| 160 |
-
# Truncated list for efficiency; add additional sites as needed
|
| 161 |
]
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
async def scrape_myscheme(page, site_config):
|
| 164 |
"""Specialized scraper for MyScheme portal"""
|
| 165 |
try:
|
| 166 |
logger.info(f"π― Scraping {site_config['name']}...")
|
| 167 |
-
await asyncio.sleep(random.uniform(1.5, 3.5))
|
| 168 |
-
await page.goto(site_config["url"], timeout=
|
| 169 |
|
| 170 |
-
# Human-like scrolling
|
| 171 |
await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 3)")
|
| 172 |
await asyncio.sleep(random.uniform(0.8, 1.8))
|
| 173 |
|
|
@@ -178,25 +205,18 @@ async def scrape_myscheme(page, site_config):
|
|
| 178 |
if len(schemes) >= 2:
|
| 179 |
for i in range(0, len(schemes) - 1, 2):
|
| 180 |
try:
|
| 181 |
-
|
| 182 |
-
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
"
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
"file_url": site_config["url"],
|
| 194 |
-
"authentic_source_url": site_config["source"],
|
| 195 |
-
"content_hash": content_hash
|
| 196 |
-
}
|
| 197 |
-
supabase.table("official_documents").insert(data_to_save).execute()
|
| 198 |
-
logger.info(f"β
Uploaded: {schemes[i]}")
|
| 199 |
-
results.append(True)
|
| 200 |
except Exception as e:
|
| 201 |
logger.error(f"β Failed to upload {schemes[i]}: {str(e)}")
|
| 202 |
results.append(False)
|
|
@@ -211,7 +231,7 @@ async def scrape_generic(page, site_config):
|
|
| 211 |
try:
|
| 212 |
logger.info(f"π― Scraping {site_config['name']}...")
|
| 213 |
await asyncio.sleep(random.uniform(1.5, 3.5))
|
| 214 |
-
await page.goto(site_config["url"], timeout=
|
| 215 |
|
| 216 |
await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 3)")
|
| 217 |
await asyncio.sleep(random.uniform(0.8, 1.8))
|
|
@@ -229,25 +249,17 @@ async def scrape_generic(page, site_config):
|
|
| 229 |
if all_text:
|
| 230 |
for i, text in enumerate(all_text[:10]):
|
| 231 |
try:
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
"document_type": "Government Content",
|
| 242 |
-
"file_url": site_config["url"],
|
| 243 |
-
"authentic_source_url": site_config["source"],
|
| 244 |
-
"content_hash": content_hash
|
| 245 |
-
}
|
| 246 |
-
supabase.table("official_documents").insert(data_to_save).execute()
|
| 247 |
-
logger.info(f"β
Uploaded from {site_config['name']}: {text[:50]}...")
|
| 248 |
-
results.append(True)
|
| 249 |
except Exception as e:
|
| 250 |
-
logger.warning(f"β οΈ
|
| 251 |
results.append(False)
|
| 252 |
return results
|
| 253 |
return []
|
|
@@ -256,10 +268,10 @@ async def scrape_generic(page, site_config):
|
|
| 256 |
return []
|
| 257 |
|
| 258 |
async def scrape_and_upload():
|
| 259 |
-
print("π₯· Booting up GovBridge
|
| 260 |
|
| 261 |
async with async_playwright() as p:
|
| 262 |
-
# Launch hardened browser
|
| 263 |
browser = await p.chromium.launch(
|
| 264 |
headless=True,
|
| 265 |
args=[
|
|
@@ -314,7 +326,7 @@ async def scrape_and_upload():
|
|
| 314 |
await asyncio.sleep(random.uniform(2.0, 4.0)) # Delay between sites
|
| 315 |
|
| 316 |
print(f"\n--- π FINAL REPORT ---")
|
| 317 |
-
print(f"β
Total items
|
| 318 |
print("\nShutting down bot.")
|
| 319 |
await browser.close()
|
| 320 |
|
|
|
|
| 2 |
import asyncio
|
| 3 |
import hashlib
|
| 4 |
import random
|
| 5 |
+
import httpx
|
| 6 |
from playwright.async_api import async_playwright
|
|
|
|
| 7 |
import logging
|
| 8 |
|
| 9 |
# Setup logging
|
| 10 |
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
| 11 |
logger = logging.getLogger(__name__)
|
| 12 |
|
| 13 |
+
# --- UNIFIED INGESTION CONFIGURATION (OPTION A) ---
|
| 14 |
+
HF_SPACE_URL = os.environ.get("HF_SPACE_URL", "https://harshrawat18-govbridge-api.hf.space")
|
| 15 |
+
ADMIN_SECRET = os.environ.get("ADMIN_SECRET", "")
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
# --- STEALTH SCRIPT COLLECTION ---
|
| 18 |
STEALTH_SCRIPTS = [
|
|
|
|
| 145 |
"url": "https://www.myscheme.gov.in/search",
|
| 146 |
"source": "MyScheme Portal",
|
| 147 |
"selector": "h2",
|
| 148 |
+
"extract_type": "myscheme",
|
| 149 |
+
"doc_type": "scheme"
|
| 150 |
},
|
| 151 |
{
|
| 152 |
"name": "PM-KISAN Scheme",
|
| 153 |
"url": "https://pmkisan.gov.in/",
|
| 154 |
"source": "Ministry of Agriculture",
|
| 155 |
"selector": "h2, h3, div.content",
|
| 156 |
+
"extract_type": "generic",
|
| 157 |
+
"doc_type": "scheme"
|
| 158 |
},
|
|
|
|
| 159 |
]
|
| 160 |
|
| 161 |
+
async def push_to_hf_api(title: str, text: str, source_url: str, ministry: str, doc_type: str):
|
| 162 |
+
"""Pushes scraped data directly to the Hugging Face Ingestion Engine"""
|
| 163 |
+
payload = {
|
| 164 |
+
"title": title,
|
| 165 |
+
"text": text,
|
| 166 |
+
"ministry": ministry,
|
| 167 |
+
"state": "National",
|
| 168 |
+
"source_url": source_url,
|
| 169 |
+
"doc_type": doc_type
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
async with httpx.AsyncClient() as client:
|
| 173 |
+
try:
|
| 174 |
+
resp = await client.post(
|
| 175 |
+
f"{HF_SPACE_URL}/api/admin/ingest",
|
| 176 |
+
json=payload,
|
| 177 |
+
params={"admin_key": ADMIN_SECRET},
|
| 178 |
+
headers={"X-Admin-Secret": ADMIN_SECRET},
|
| 179 |
+
timeout=30.0
|
| 180 |
+
)
|
| 181 |
+
if resp.status_code == 200:
|
| 182 |
+
logger.info(f"β
Ingested to Vector DB: {title[:40]}...")
|
| 183 |
+
return True
|
| 184 |
+
else:
|
| 185 |
+
logger.error(f"β HF API Error: {resp.status_code} - {resp.text}")
|
| 186 |
+
return False
|
| 187 |
+
except Exception as e:
|
| 188 |
+
logger.error(f"β οΈ Network Error pushing {title[:30]}: {e}")
|
| 189 |
+
return False
|
| 190 |
+
|
| 191 |
async def scrape_myscheme(page, site_config):
|
| 192 |
"""Specialized scraper for MyScheme portal"""
|
| 193 |
try:
|
| 194 |
logger.info(f"π― Scraping {site_config['name']}...")
|
| 195 |
+
await asyncio.sleep(random.uniform(1.5, 3.5))
|
| 196 |
+
await page.goto(site_config["url"], timeout=60000, wait_until="domcontentloaded")
|
| 197 |
|
|
|
|
| 198 |
await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 3)")
|
| 199 |
await asyncio.sleep(random.uniform(0.8, 1.8))
|
| 200 |
|
|
|
|
| 205 |
if len(schemes) >= 2:
|
| 206 |
for i in range(0, len(schemes) - 1, 2):
|
| 207 |
try:
|
| 208 |
+
title = schemes[i]
|
| 209 |
+
department = schemes[i + 1] if i + 1 < len(schemes) else "Unknown"
|
| 210 |
|
| 211 |
+
# Push directly to HF Engine
|
| 212 |
+
success = await push_to_hf_api(
|
| 213 |
+
title=title,
|
| 214 |
+
text=f"Details regarding {title} provided by {department}.",
|
| 215 |
+
source_url=site_config["url"],
|
| 216 |
+
ministry=department,
|
| 217 |
+
doc_type=site_config["doc_type"]
|
| 218 |
+
)
|
| 219 |
+
results.append(success)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
except Exception as e:
|
| 221 |
logger.error(f"β Failed to upload {schemes[i]}: {str(e)}")
|
| 222 |
results.append(False)
|
|
|
|
| 231 |
try:
|
| 232 |
logger.info(f"π― Scraping {site_config['name']}...")
|
| 233 |
await asyncio.sleep(random.uniform(1.5, 3.5))
|
| 234 |
+
await page.goto(site_config["url"], timeout=60000, wait_until="domcontentloaded")
|
| 235 |
|
| 236 |
await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 3)")
|
| 237 |
await asyncio.sleep(random.uniform(0.8, 1.8))
|
|
|
|
| 249 |
if all_text:
|
| 250 |
for i, text in enumerate(all_text[:10]):
|
| 251 |
try:
|
| 252 |
+
# Push directly to HF Engine
|
| 253 |
+
success = await push_to_hf_api(
|
| 254 |
+
title=text,
|
| 255 |
+
text=text,
|
| 256 |
+
source_url=site_config["url"],
|
| 257 |
+
ministry=site_config["source"],
|
| 258 |
+
doc_type=site_config["doc_type"]
|
| 259 |
+
)
|
| 260 |
+
results.append(success)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
except Exception as e:
|
| 262 |
+
logger.warning(f"β οΈ Error: {str(e)[:50]}")
|
| 263 |
results.append(False)
|
| 264 |
return results
|
| 265 |
return []
|
|
|
|
| 268 |
return []
|
| 269 |
|
| 270 |
async def scrape_and_upload():
|
| 271 |
+
print("π₯· Booting up GovBridge UNIFIED Scraper (Sending to HF API)...\n")
|
| 272 |
|
| 273 |
async with async_playwright() as p:
|
| 274 |
+
# Launch hardened browser
|
| 275 |
browser = await p.chromium.launch(
|
| 276 |
headless=True,
|
| 277 |
args=[
|
|
|
|
| 326 |
await asyncio.sleep(random.uniform(2.0, 4.0)) # Delay between sites
|
| 327 |
|
| 328 |
print(f"\n--- π FINAL REPORT ---")
|
| 329 |
+
print(f"β
Total items pushed to Vector Engine: {total_uploaded}")
|
| 330 |
print("\nShutting down bot.")
|
| 331 |
await browser.close()
|
| 332 |
|