File size: 4,060 Bytes
2d2d299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/usr/bin/env python3
"""
extract_rules.py β€” Extract structured rules from classical astrology texts.

Pipeline
--------
1. Load all ``.txt`` / ``.pdf`` files from ``data/raw/texts/``.
2. Chunk them with ``SmartChunker`` (verse-aware then sliding-window fallback).
3. For each chunk, run ``RuleExtractor`` (LLM extraction with regex fallback).
4. Deduplicate by rule_id and save to ``data/processed/rules.json``.

The extraction step caches results per chunk in Redis so re-runs only
process new or changed chunks.  Pass ``--no-llm`` to skip LLM and use
regex-only extraction (useful offline or when API key is not set).

Run with:
    make ingest-rules
    uv run python scripts/extract_rules.py [--no-llm] [--limit N]
"""

from __future__ import annotations

import argparse
import asyncio
import json
import logging
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logger = logging.getLogger(__name__)

RAW_DIR   = Path("data/raw/texts")
OUT_PATH  = Path("data/processed/rules.json")


async def main(use_llm: bool = True, limit: int = 0, concurrency: int = 6) -> None:
    from vedic_astro.rag.loaders import DirectoryLoader
    from vedic_astro.rag.chunker import SmartChunker
    from vedic_astro.rag.rule_extractor import RuleExtractor

    # ── 1. Load documents ─────────────────────────────────────────────────
    if not RAW_DIR.exists():
        logger.warning("Text directory not found: %s", RAW_DIR)
        logger.warning("Place classical text files (*.txt / *.pdf) in data/raw/texts/")
        OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
        OUT_PATH.write_text("[]")
        return

    docs = DirectoryLoader(RAW_DIR).load()
    if not docs:
        logger.warning("No documents found in %s", RAW_DIR)
        OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
        OUT_PATH.write_text("[]")
        return

    logger.info("Loaded %d documents", len(docs))

    # ── 2. Chunk ──────────────────────────────────────────────────────────
    chunks = SmartChunker().chunk(docs)
    logger.info("Chunked into %d chunks", len(chunks))

    if limit > 0:
        chunks = chunks[:limit]
        logger.info("Limited to %d chunks", len(chunks))

    # ── 3. Extract rules ──────────────────────────────────────────────────
    extractor = RuleExtractor(use_llm=use_llm)
    rules = await extractor.extract_batch(chunks, concurrency=concurrency)

    # ── 4. Merge with any existing rules (for incremental runs) ──────────
    existing: list[dict] = []
    if OUT_PATH.exists():
        try:
            existing = json.loads(OUT_PATH.read_text())
        except (json.JSONDecodeError, OSError):
            existing = []

    existing_ids = {r.get("rule_id") for r in existing}
    new_rules = [r.model_dump() for r in rules if r.rule_id not in existing_ids]
    all_rules = existing + new_rules

    OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
    OUT_PATH.write_text(json.dumps(all_rules, indent=2, ensure_ascii=False))
    logger.info(
        "Saved %d rules (%d new, %d existing) β†’ %s",
        len(all_rules), len(new_rules), len(existing), OUT_PATH,
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--no-llm", action="store_true", help="Use regex-only extraction")
    parser.add_argument("--limit", type=int, default=0, help="Max chunks to process (0=all)")
    parser.add_argument("--concurrency", type=int, default=6)
    args = parser.parse_args()

    asyncio.run(main(
        use_llm=not args.no_llm,
        limit=args.limit,
        concurrency=args.concurrency,
    ))