Download scripts/resolve_relevant_chunks.py from Prabu17/askmydocs: direct link, hf CLI and curl.
- Browser
- Download file 5.33 kB
-
https://huggingface.co/spaces/Prabu17/askmydocs/resolve/main/scripts/resolve_relevant_chunks.py
- Command line
-
hf download hf://spaces/Prabu17/askmydocs/scripts/resolve_relevant_chunks.py
-
curl -L -o resolve_relevant_chunks.py https://huggingface.co/spaces/Prabu17/askmydocs/resolve/main/scripts/resolve_relevant_chunks.py
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()) |