SAP-ERP-AI-Agent / src /rag /build_eval_ids.py
daisysooyeon's picture
deploy: SAP ERP AI Agent (HF Spaces docker)
50efdc6
Raw
History Blame Contribute Delete
6.04 kB
"""
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()