askmydocs / scripts /resolve_relevant_chunks.py
Prabu17's picture
Upload folder using huggingface_hub
f8b48da verified
Raw History Blame Contribute Delete
5.33 kB
"""Resolve golden dataset chunk references against the real chunker output.
The golden dataset was authored with *approximate* chunk indices. This script:
1. Loads every corpus markdown document.
2. Runs the real paragraph-aware chunker (``app.ingestion.chunker``).
3. Rebuilds ``relevant_chunk_ids`` per question by matching the expected
answer's key phrases against the chunk texts (best-match chunk).
4. Rewrites ``evals/dataset/golden.jsonl`` in place.
This keeps the dataset truthful: every referenced chunk actually contains
the evidence that answers the question.
Usage:
python scripts/resolve_relevant_chunks.py [--write]
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from app.ingestion.chunker import Chunker, ChunkingStrategy # noqa: E402
QUESTION_CHUNK_OVERRIDES: dict[str, list[str]] = {}
def _tokenize(text: str) -> set[str]:
return set(re.findall(r"[a-z0-9_]+", text.lower()))
def _ngram_keywords(text: str, n: int = 3) -> list[str]:
"""Extract salient n-gram phrases for matching."""
tokens = _tokenize(text)
# Remove very common filler words for matching robustness.
stop = {
"the", "a", "an", "of", "for", "to", "and", "or", "in", "on", "is",
"are", "what", "which", "how", "does", "do", "whats", "what's", "your",
}
tokens = tokens - stop
return list(tokens)
def _match_score(chunk_text: str, keywords: list[str]) -> float:
"""Jaccard-like overlap between chunk tokens and question keywords."""
chunk_tokens = _tokenize(chunk_text)
if not keywords or not chunk_tokens:
return 0.0
hit = sum(1 for k in keywords if k in chunk_tokens)
return hit / len(keywords)
def resolve_for_document(
doc_id: str,
chunks: list,
question: str,
expected_answer: str,
) -> str | None:
"""Return the chunk id whose text best matches the expected answer."""
answer_keywords = _ngram_keywords(expected_answer or question)
candidates = chunks
# Prefer chunks that contain answer-specific token matches.
scored = [
(chunk, _match_score(chunk.text, answer_keywords) + _match_score(chunk.text, _ngram_keywords(question)))
for chunk in candidates
]
scored.sort(key=lambda pair: pair[1], reverse=True)
best_chunk, best_score = scored[0]
if best_score <= 0.0:
return None
return best_chunk.chunk_id
def main() -> int:
parser = argparse.ArgumentParser(description="Resolve golden dataset chunk references.")
parser.add_argument("--write", action="store_true", help="Rewrite golden.jsonl in place")
args = parser.parse_args()
corpus_dir = ROOT / "data" / "corpus" / "markdown"
dataset_path = ROOT / "evals" / "dataset" / "golden.jsonl"
chunker = Chunker(strategy=ChunkingStrategy.PARAGRAPH, chunk_size=512, chunk_overlap=64)
# Chunk every markdown document once.
doc_chunks: dict[str, list] = {}
for md_path in sorted(corpus_dir.glob("*.md")):
doc_id = md_path.stem
text = md_path.read_text(encoding="utf-8")
chunks = chunker.chunk_text(
text=text,
document_id=doc_id,
document_name=doc_id,
source=f"markdown/{md_path.name}",
)
doc_chunks[doc_id] = chunks
lines = dataset_path.read_text(encoding="utf-8").splitlines()
updated: list[str] = []
changes = 0
unresolved: list[str] = []
for line in lines:
if not line.strip():
continue
record = json.loads(line)
if record.get("difficulty") == "unanswerable":
updated.append(line)
continue
new_refs: list[str] = []
for ref in record.get("relevant_chunk_ids", []):
doc_id, _, _ = ref.rpartition(":")
chunks = doc_chunks.get(doc_id)
if chunks is None:
unresolved.append(f"{record['id']}: unknown doc '{doc_id}'")
continue
matched = resolve_for_document(
doc_id, chunks, record["question"], record.get("expected_answer", "")
)
if matched is None:
unresolved.append(f"{record['id']}: no match in '{doc_id}'")
continue
if matched not in new_refs:
new_refs.append(matched)
if new_refs != record.get("relevant_chunk_ids", []):
changes += 1
record["relevant_chunk_ids"] = new_refs
updated.append(json.dumps(record, ensure_ascii=False))
# Report chunk inventory per document for verification.
print(f"Processed {len(updated)} records; {changes} changed; {len(unresolved)} unresolved.")
if unresolved:
for u in unresolved:
print(f" UNRESOLVED: {u}")
return 1
if args.write:
dataset_path.write_text("\n".join(updated) + "\n", encoding="utf-8")
print(f"Wrote {dataset_path}")
else:
print("Dry-run: no file written (use --write to commit).")
return 0
if __name__ == "__main__":
raise SystemExit(main())