| |
| """ |
| Ingest rekt.news hack data into RAG known_scams collection. |
| |
| Usage: |
| # Via HTTP API (from host or any networked client): |
| python3 scripts/ingest_rekt_hacks.py --api |
| |
| # Direct Python import (run inside Docker container): |
| docker exec rmi-backend python3 scripts/ingest_rekt_hacks.py --direct |
| |
| # Dry run (print what would be ingested): |
| python3 scripts/ingest_rekt_hacks.py --dry-run |
| """ |
|
|
| import argparse |
| import asyncio |
| import json |
| import os |
| import sys |
| import time |
|
|
| |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| DATA_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "rekt_hacks.json") |
| API_URL = os.getenv("RAG_API_URL", "http://localhost:8000/api/v1/rag/ingest") |
| COLLECTION = "known_scams" |
|
|
|
|
| def load_hacks(): |
| with open(DATA_FILE) as f: |
| return json.load(f) |
|
|
|
|
| def build_doc_text(hack): |
| """Build a rich text document for semantic search.""" |
| parts = [] |
| parts.append(f"Project: {hack.get('name')}") |
| if hack.get("date"): |
| parts.append(f"Date: {hack['date']}") |
| parts.append(f"Amount Lost: ${hack['amount_lost']:,.0f}") |
| if hack.get("chain"): |
| parts.append(f"Chain: {hack['chain']}") |
| if hack.get("attack_type"): |
| parts.append(f"Attack Type: {hack['attack_type']}") |
| if hack.get("audit_status"): |
| parts.append(f"Audit Status: {hack['audit_status']}") |
| if hack.get("description"): |
| parts.append(f"Description: {hack['description']}") |
| if hack.get("tags"): |
| parts.append(f"Tags: {', '.join(hack['tags'])}") |
| if hack.get("url"): |
| parts.append(f"URL: {hack['url']}") |
| return "\n".join(parts) |
|
|
|
|
| def build_metadata(hack): |
| """Build metadata dict for the RAG document.""" |
| meta = { |
| "source": "rekt_news", |
| "name": hack.get("name"), |
| "amount_lost": hack["amount_lost"], |
| } |
| if hack.get("date"): |
| meta["date"] = hack["date"] |
| if hack.get("chain"): |
| meta["chain"] = hack["chain"] |
| if hack.get("attack_type"): |
| meta["attack_type"] = hack["attack_type"] |
| if hack.get("audit_status"): |
| meta["audit_status"] = hack["audit_status"] |
| if hack.get("url"): |
| meta["url"] = hack["url"] |
| if hack.get("slug"): |
| meta["slug"] = hack["slug"] |
| if hack.get("tags"): |
| meta["tags"] = hack["tags"] |
|
|
| |
| amt = hack["amount_lost"] |
| if amt >= 100_000_000: |
| meta["severity"] = "critical" |
| elif amt >= 10_000_000: |
| meta["severity"] = "high" |
| elif amt >= 1_000_000: |
| meta["severity"] = "medium" |
| else: |
| meta["severity"] = "low" |
|
|
| return meta |
|
|
|
|
| def make_doc_id(hack): |
| """Generate deterministic doc ID from slug.""" |
| slug = hack.get("slug") or hack.get("name").lower().replace(" ", "-").replace("/", "-") |
| |
| slug = "".join(c for c in slug if c.isalnum() or c in "-_").strip("-") |
| return f"rekt-{slug}" |
|
|
|
|
| |
| |
| |
| async def ingest_via_api(hacks, dry_run=False): |
| import urllib.request |
|
|
| success = 0 |
| failed = 0 |
|
|
| for i, hack in enumerate(hacks): |
| doc_id = make_doc_id(hack) |
| text = build_doc_text(hack) |
| metadata = build_metadata(hack) |
|
|
| payload = json.dumps( |
| { |
| "collection": COLLECTION, |
| "content": text, |
| "metadata": metadata, |
| "doc_id": doc_id, |
| } |
| ).encode("utf-8") |
|
|
| if dry_run: |
| if i < 3: |
| print(f"[DRY RUN] Would ingest: {doc_id} | {hack.get('name')} | ${hack['amount_lost']:,.0f}") |
| continue |
|
|
| req = urllib.request.Request( |
| API_URL, |
| data=payload, |
| headers={"Content-Type": "application/json"}, |
| method="POST", |
| ) |
| try: |
| resp = urllib.request.urlopen(req, timeout=30) |
| json.loads(resp.read()) |
| success += 1 |
| if (i + 1) % 20 == 0: |
| print(f" Progress: {i + 1}/{len(hacks)} ingested ({success} ok, {failed} fail)") |
| except Exception as e: |
| failed += 1 |
| print(f" FAILED {doc_id}: {e}") |
|
|
| |
| if (i + 1) % 10 == 0: |
| time.sleep(0.5) |
|
|
| return success, failed |
|
|
|
|
| |
| |
| |
| async def ingest_direct(hacks, dry_run=False): |
| from app.rag_service import ingest_document |
|
|
| success = 0 |
| failed = 0 |
|
|
| for i, hack in enumerate(hacks): |
| doc_id = make_doc_id(hack) |
| text = build_doc_text(hack) |
| metadata = build_metadata(hack) |
|
|
| if dry_run: |
| if i < 3: |
| print(f"[DRY RUN] Would ingest: {doc_id} | {hack.get('name')} | ${hack['amount_lost']:,.0f}") |
| continue |
|
|
| try: |
| await ingest_document( |
| collection=COLLECTION, |
| content=text, |
| metadata=metadata, |
| doc_id=doc_id, |
| ) |
| success += 1 |
| if (i + 1) % 20 == 0: |
| print(f" Progress: {i + 1}/{len(hacks)} ingested ({success} ok, {failed} fail)") |
| except Exception as e: |
| failed += 1 |
| print(f" FAILED {doc_id}: {e}") |
|
|
| |
| if (i + 1) % 10 == 0: |
| await asyncio.sleep(0.1) |
|
|
| return success, failed |
|
|
|
|
| async def main(): |
| parser = argparse.ArgumentParser(description="Ingest rekt.news hacks into RAG") |
| parser.add_argument("--api", action="store_true", help="Use HTTP API (default if running from host)") |
| parser.add_argument( |
| "--direct", |
| action="store_true", |
| help="Use direct Python import (run inside Docker container)", |
| ) |
| parser.add_argument( |
| "--dry-run", |
| action="store_true", |
| help="Print what would be ingested without actually ingesting", |
| ) |
| parser.add_argument("--limit", type=int, default=0, help="Limit number of hacks to ingest (0 = all)") |
| args = parser.parse_args() |
|
|
| hacks = load_hacks() |
| print(f"Loaded {len(hacks)} hacks from {DATA_FILE}") |
|
|
| if args.limit > 0: |
| hacks = hacks[: args.limit] |
| print(f"Limiting to first {args.limit} hacks") |
|
|
| |
| if not args.direct: |
| args.api = True |
|
|
| if args.dry_run: |
| print("\n=== DRY RUN ===") |
| for hack in hacks[:5]: |
| doc_id = make_doc_id(hack) |
| text = build_doc_text(hack) |
| metadata = build_metadata(hack) |
| print(f"\n--- {doc_id} ---") |
| print(f"Text preview: {text[:200]}...") |
| print(f"Metadata: {json.dumps(metadata, indent=2)[:300]}...") |
| if len(hacks) > 5: |
| print(f"\n... and {len(hacks) - 5} more") |
| return |
|
|
| method = "HTTP API" if args.api else "Direct Python" |
| print(f"\nIngesting {len(hacks)} hacks via {method}...") |
| print(f"Collection: {COLLECTION}") |
| print() |
|
|
| if args.api: |
| success, failed = await ingest_via_api(hacks) |
| else: |
| success, failed = await ingest_direct(hacks) |
|
|
| print("\n=== DONE ===") |
| print(f"Success: {success}") |
| print(f"Failed: {failed}") |
| print(f"Total: {len(hacks)}") |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|