Spaces:
Sleeping
Sleeping
| """ | |
| src/rag/build_eval_ids.py | |
| ํ๊ฐ ๋ฐ์ดํฐ์ ์ rag_evidence(์ ๋ต ์ฒญํฌ ํ ์คํธ) โ ingest๋ ์ฒญํฌ์ chunk_id ๋งคํ ๋น๋. | |
| eval_worker_b ๊ฐ ํ ์คํธ fuzzy ๋งค์นญ ๋์ chunk_id ์ ํ ๋งค์นญ์ผ๋ก RAG ์ฑ๋ฅ์ ํ๊ฐํ๋ ค๋ฉด, | |
| ๊ฐ QA ํ ์คํธ ์ผ์ด์ค์ ์ ๋ต chunk_id ๊ฐ ๋ฏธ๋ฆฌ ๋ถ์ฌ๋ผ ์์ด์ผ ํ๋ค. ์ด ์คํฌ๋ฆฝํธ๋ | |
| ChromaDB ์ปฌ๋ ์ (= ingest ๊ฒฐ๊ณผ)์ ์ฝ์ด ๊ฐ ์ผ์ด์ค์ evidence ํ ์คํธ๊ฐ ์ด๋ ์ฒญํฌ์์ | |
| ๋์จ ๊ฒ์ธ์ง ์ฐพ์ `evidence_chunk_id` ํ๋๋ก ๋ฐ์ดํฐ์ ์ ๊ธฐ๋กํ๋ค. | |
| ์ ์ : ingest_documents() ๋ฅผ ๋จผ์ ์คํํด ์ฒญํฌ์ ์ ์ผ chunk_id ๊ฐ ๋ถ์ฌ๋ผ ์์ด์ผ ํ๋ค. | |
| CLI: | |
| python -m src.rag.build_eval_ids # ๋ฐ์ดํฐ์ in-place ๊ฐฑ์ | |
| python -m src.rag.build_eval_ids --out data/eval/with_ids.json | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import sys | |
| from pathlib import Path | |
| import chromadb | |
| from src.config import get_config | |
| logger = logging.getLogger(__name__) | |
| QA_LABELS = ("QA_ONLY", "BOTH") | |
| def _norm(s: str) -> str: | |
| """๊ณต๋ฐฑ ์ ๊ทํ โ ์ค๋ฐ๊ฟ/์ฐ์๊ณต๋ฐฑ ์ฐจ์ด๋ฅผ ๋ฌด์ํ๊ณ ๋ด์ฉ๋ง ๋น๊ต.""" | |
| return " ".join((s or "").split()) | |
| def _match_chunk_id(evidence: str, corpus: list[tuple[str, str]], threshold: float = 0.5) -> str | None: | |
| """ | |
| evidence ํ ์คํธ๊ฐ ๋์จ ์ฒญํฌ์ chunk_id ๋ฅผ ๋ฐํ. ์์ผ๋ฉด None. | |
| rag_evidence ๋ ingest๋ ์ฒญํฌ์ page_content(ํค๋ `[Source: ...]` ํฌํจ)๋ฅผ ๊ทธ๋๋ก | |
| ๋ณต์ฌํ ๊ฐ์ด๋ฏ๋ก, 1์์๋ก *์ ์ฒด ๋ด์ฉ ์ ํ ์ผ์น*(๊ณต๋ฐฑ ์ ๊ทํ)๋ก ๋งค์นญํ๋ค. ์ด๋ ๊ฒ ํ๋ฉด | |
| OCR/๋นOCR ๋ณํ๋ณธ์ฒ๋ผ ์๋ถ๋ถ(ํค๋)์ด ๋์ผํ ์ฒญํฌ๋ผ๋ฆฌ๋ ๋ณธ๋ฌธ ์ฐจ์ด๋ก ์ ํํ ๊ตฌ๋ถ๋๋ค. | |
| ์ ํ ์ผ์น๊ฐ ์์ ๋๋ง substring โ ํ ํฐ overlap ์ผ๋ก ํด๋ฐฑํ๋ค. | |
| (ํค๋๋ ๋ณํ๋ณธ ๊ตฌ๋ถ์ ํ์ํ ์ ๋ณด์ด๋ฏ๋ก ์ ๊ฑฐํ์ง ์๋๋ค.) | |
| """ | |
| if not evidence: | |
| return None | |
| ev_norm = _norm(evidence) | |
| # 1์์: ์ ์ฒด ๋ด์ฉ ์ ํ ์ผ์น (ํค๋ ํฌํจ). ๋ณํ๋ณธ ํผ๋ ์์ด ์ ์ผ ์ฒญํฌ ํ์ . | |
| for chunk_id, content in corpus: | |
| if ev_norm and _norm(content) == ev_norm: | |
| return chunk_id | |
| # 2์์: evidence ์ ์ฒด๊ฐ ์ฒญํฌ์ substring (์ ๊ทํ ๊ธฐ์ค) | |
| for chunk_id, content in corpus: | |
| if ev_norm and ev_norm in _norm(content): | |
| return chunk_id | |
| # 3์์: ํ ํฐ overlap ํด๋ฐฑ | |
| gt_tokens = set(evidence.lower().split()) | |
| best: str | None = None | |
| best_overlap = 0.0 | |
| for chunk_id, content in corpus: | |
| doc_tokens = set(content.lower().split()) | |
| if gt_tokens and doc_tokens: | |
| overlap = len(gt_tokens & doc_tokens) / len(gt_tokens) | |
| if overlap >= threshold and overlap > best_overlap: | |
| best_overlap = overlap | |
| best = chunk_id | |
| return best | |
| def _load_corpus() -> list[tuple[str, str]]: | |
| """ChromaDB ์ปฌ๋ ์ ์์ (chunk_id, content) ์ ์ฒด๋ฅผ ๋ก๋.""" | |
| cfg = get_config() | |
| client = chromadb.PersistentClient(path=cfg.paths.chroma_db) | |
| col = client.get_collection(cfg.rag.collection_name) | |
| raw = col.get(include=["documents", "metadatas"]) | |
| corpus: list[tuple[str, str]] = [] | |
| for cid, content, meta in zip(raw["ids"], raw["documents"], raw["metadatas"]): | |
| # metadata ์ chunk_id ๋ฅผ ์ฐ์ ์ฌ์ฉํ๊ณ , ์์ผ๋ฉด vector-store id ๋ก ํด๋ฐฑ | |
| chunk_id = (meta or {}).get("chunk_id") or cid | |
| corpus.append((chunk_id, content or "")) | |
| return corpus | |
| def build_eval_ids(dataset_path: Path, out_path: Path) -> dict: | |
| cases: list[dict] = json.loads(dataset_path.read_text(encoding="utf-8")) | |
| corpus = _load_corpus() | |
| logger.info("Corpus loaded: %d chunks", len(corpus)) | |
| matched = unmatched = skipped = 0 | |
| for case in cases: | |
| if case.get("label") not in QA_LABELS: | |
| continue | |
| evidence = case.get("rag_evidence") | |
| if not evidence: | |
| skipped += 1 | |
| continue | |
| evidences = evidence if isinstance(evidence, list) else [evidence] | |
| ids = [cid for ev in evidences if (cid := _match_chunk_id(ev, corpus)) is not None] | |
| ids = list(dict.fromkeys(ids)) # ์์ ์ ์ง dedup | |
| if ids: | |
| # ๋จ์ผ์ด๋ฉด str, ๋ค์ค์ด๋ฉด list ๋ก ์ ์ฅ (eval ์ ๋ ๋ค ์ฒ๋ฆฌ) | |
| case["evidence_chunk_id"] = ids[0] if len(ids) == 1 else ids | |
| matched += 1 | |
| else: | |
| case.pop("evidence_chunk_id", None) # ๋งค์นญ ์คํจ ์ ์ ๊ฑฐ โ eval ์ ํ ์คํธ ๋งค์นญ์ผ๋ก ํด๋ฐฑ | |
| unmatched += 1 | |
| logger.warning("[%s] evidence โ chunk_id ๋งค์นญ ์คํจ (eval ํ ์คํธ ํด๋ฐฑ)", case.get("id")) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| out_path.write_text(json.dumps(cases, indent=2, ensure_ascii=False), encoding="utf-8") | |
| stats = {"matched": matched, "unmatched": unmatched, "skipped_no_evidence": skipped} | |
| return stats | |
| def _main(): | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| except Exception: | |
| pass | |
| from src.logging_config import setup_logging | |
| setup_logging() | |
| cfg = get_config() | |
| parser = argparse.ArgumentParser(description="ํ๊ฐ ๋ฐ์ดํฐ์ ์ ์ ๋ต chunk_id ๋ถ์ฌ") | |
| parser.add_argument("--dataset", default="data/eval/router_test_cases_gen.json") | |
| parser.add_argument("--out", default=None, help="์ถ๋ ฅ ๊ฒฝ๋ก (๊ธฐ๋ณธ: ์ ๋ ฅ ํ์ผ in-place)") | |
| args = parser.parse_args() | |
| dataset_path = Path(args.dataset) | |
| out_path = Path(args.out) if args.out else dataset_path | |
| stats = build_eval_ids(dataset_path, out_path) | |
| print(f"\n{'='*60}") | |
| print(" evidence โ chunk_id ๋งคํ ์๋ฃ") | |
| print(f"{'='*60}") | |
| print(f" matched : {stats['matched']}") | |
| print(f" unmatched (ํด๋ฐฑ) : {stats['unmatched']}") | |
| print(f" no evidence (skip) : {stats['skipped_no_evidence']}") | |
| print(f" saved to : {out_path}") | |
| print(f"{'='*60}\n") | |
| if __name__ == "__main__": | |
| _main() | |