ai-tutor-chatbot / data /scraping_scripts /graphrag_prep_input.py
Louis-François Bouchard
compaction study: harness for compaction-vs-keep-everything on a long lesson
0ad2e61
Raw History Blame
4.37 kB
"""Prepare GraphRAG input from the existing corpus.
Emits one JSONL row per corpus document into the GraphRAG workspace input dir,
using the canonical raw markdown mirrors as text and the KB manifest as the
metadata source of truth. The row ``id`` is set to our ``doc_id`` so GraphRAG's
``text_units.document_ids`` map straight back to ``corpus_manifest.jsonl``
(source / url / lesson) at retrieval time -- no GraphRAG metadata propagation
needed.
This is part of the GraphRAG-vs-RAG experiment (branch experiment/graphrag-vs-rag).
The workspace under data/graphrag/ is local-only and never uploaded to the HF
vector-db bundle.
uv run -m data.scraping_scripts.graphrag_prep_input # full corpus
uv run -m data.scraping_scripts.graphrag_prep_input --limit 10 # smoke subset
"""
from __future__ import annotations
import argparse
import json
import logging
from pathlib import Path
logger = logging.getLogger("graphrag_prep_input")
MANIFEST_PATH = "data/kb/generated/corpus_manifest.jsonl"
DEFAULT_OUT = "data/graphrag/input/corpus.jsonl"
def load_manifest(path: str) -> list[dict]:
records: list[dict] = []
with open(path, encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line:
records.append(json.loads(line))
return records
def read_doc_text(record: dict) -> str:
"""Canonical raw markdown for a manifest record, or '' if unreadable."""
path = record.get("path")
if not path:
return ""
try:
return Path(path).read_text(encoding="utf-8").strip()
except OSError as exc:
logger.warning(
"Skipping %s: cannot read %s (%s)", record.get("doc_id"), path, exc
)
return ""
def build_rows(records: list[dict], sources: set[str] | None = None) -> list[dict]:
rows: list[dict] = []
for record in records:
if sources and record.get("source") not in sources:
continue
text = read_doc_text(record)
if not text:
continue
rows.append(
{
"id": record["doc_id"],
"title": record.get("title") or record["doc_id"],
"text": text,
"source": record.get("source", ""),
"url": record.get("url", ""),
"source_group": record.get("source_group", ""),
}
)
return rows
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--manifest", default=MANIFEST_PATH)
parser.add_argument("--out", default=DEFAULT_OUT)
parser.add_argument(
"--limit",
type=int,
default=0,
help="First N documents only (for a cheap smoke index); 0 = all.",
)
parser.add_argument(
"--sources",
nargs="*",
default=[],
help="Restrict to these source keys (e.g. full_stack_ai_engineering); "
"empty = all sources. Used to scope the GraphRAG index to the eval "
"battery's sources and keep indexing within budget.",
)
parser.add_argument(
"--doc-ids",
nargs="*",
default=[],
help="Restrict to these exact doc_ids (e.g. a single long lesson for the "
"knowledge-compaction study).",
)
return parser.parse_args()
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
args = parse_args()
records = load_manifest(args.manifest)
if args.sources:
wanted = set(args.sources)
records = [r for r in records if r.get("source") in wanted]
if args.doc_ids:
wanted_ids = set(args.doc_ids)
records = [r for r in records if r.get("doc_id") in wanted_ids]
if args.limit:
records = records[: args.limit]
rows = build_rows(records)
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
total_tokens = sum(r.get("tokens", 0) for r in records)
logger.info(
"Wrote %d/%d docs (%.1fM manifest tokens) -> %s",
len(rows),
len(records),
total_tokens / 1e6,
out_path,
)
if __name__ == "__main__":
main()