File size: 5,325 Bytes
f8b48da | 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | """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()) |