o2o-site-AEO/solution/backend/services/grounding/extract.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

128 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""추출 결과 검증 — "AI 가 원문에서 찾아온 것인가, 지어낸 것인가" 를 코드로 가른다."""
import re
from common.category_schema import CategorySchema
# 원문 대조 전 정규화: 공백·따옴표·괄호 종류 차이로 어긋나는 것을 막는다.
_WS = re.compile(r"\s+")
_QUOTES = str.maketrans({
"“": '"', "”": '"', "‘": "'", "’": "'", """: '"',
"-": "-", "–": "-", "—": "-", "~": "~", "~": "~",
"(": "(", ")": ")", ":": ":", ",": ",", ".": ".",
})
# evidence 가 이보다 짧으면 대조가 의미 없다("3" 은 원문 어디에나 있다).
MIN_EVIDENCE = 6
# 값이 evidence 안에 있는지까지 볼 필요가 없는 타입.
_SKIP_VALUE_CHECK = {"bool"}
_FALSE_WORDS = ("false", "불가", "없음", "없슴", "미제공", "제공하지", "안됨", "안 됨", "불가능", "금지", "않습니다", "제한")
_TRUE_WORDS = ("true", "가능", "있음", "제공", "완비", "무료", "구비")
_NUMBER_OK = re.compile(r"^\d+(\.\d+)?$")
def normalize(text: str) -> str:
"""대조용 정규화."""
return _WS.sub(" ", (text or "").translate(_QUOTES)).strip().lower()
def loose(text: str) -> str:
"""값 대조 전용 — 공백과 자릿수 쉼표까지 지운다."""
return normalize(text).replace(",", "").replace(" ", "")
def _bool_ok(value: str, evidence: str) -> tuple[bool, str]:
"""bool 값의 부호가 근거 문장과 맞는가."""
v = value.strip().lower()
if v not in ("true", "false"):
return False, f"bool 필드인데 값이 'true'/'false' 가 아니다: {value!r}"
ev = evidence.lower()
has_false = any(w in ev for w in _FALSE_WORDS)
has_true = any(w in ev for w in _TRUE_WORDS)
if v == "true" and has_false and not has_true:
return False, "근거 문장은 부정인데 true 로 올렸다"
if v == "false" and has_true and not has_false:
return False, "근거 문장은 긍정인데 false 로 올렸다"
return True, ""
def verify(
rows: list[dict],
*,
source_text: str,
schema: CategorySchema,
) -> tuple[list[dict], list[tuple[str, str]]]:
"""추출 결과를 걸러 (통과, 반려) 로 나눈다."""
haystack = normalize(source_text)
passed: list[dict] = []
rejected: list[tuple[str, str]] = []
seen: set[tuple[str, str]] = set()
for row in rows:
key = str(row.get("key") or "").strip()
value = str(row.get("value") or "").strip()
unit_name = str(row.get("unit_name") or "").strip()
evidence = str(row.get("evidence") or "").strip()
label = f"{key}={value[:30]}" if key else "(key 없음)"
# 1) 업종 스키마에 있는 key 인가.
spec = schema.get(key)
if spec is None:
rejected.append((label, f"업종 스키마에 없는 key: {key!r}"))
continue
if not value:
rejected.append((label, "값이 비었다"))
continue
# 2) 스코프 계약.
if spec.scope == "unit" and not unit_name:
rejected.append((label, "unit 스코프인데 unit_name 이 없다"))
continue
if spec.scope == "place" and unit_name:
unit_name = "" # place 스코프에 이름이 붙어 온 것은 무시하고 진행한다
# 3) 근거 문장이 원문에 실제로 있는가.
if len(evidence) < MIN_EVIDENCE:
rejected.append((label, f"근거 문장이 너무 짧다({len(evidence)}자) — 대조할 수 없다"))
continue
if normalize(evidence) not in haystack:
rejected.append((label, "근거 문장이 원문에 없다 — 지어낸 값으로 본다"))
continue
# 4) 타입 계약.
if spec.type == "bool":
ok, why = _bool_ok(value, evidence)
if not ok:
rejected.append((label, why))
continue
elif spec.type == "number":
if not _NUMBER_OK.match(value.replace(",", "")):
rejected.append((label, f"number 필드인데 숫자가 아니다: {value!r}"))
continue
value = value.replace(",", "")
# 5) 값이 근거 안에 실제로 적혀 있는가(bool 제외).
if spec.type not in _SKIP_VALUE_CHECK and loose(value) not in loose(evidence):
rejected.append((label, "값이 근거 문장 안에 없다 — 원문 표기를 고쳐 쓴 것으로 본다"))
continue
# 6) 같은 (key, unit) 중복은 첫 건만.
dedupe = (key, unit_name)
if dedupe in seen:
rejected.append((label, "같은 key 가 이미 들어왔다"))
continue
seen.add(dedupe)
passed.append({
"key": key,
"value": value,
"scope": spec.scope,
"unit_name": unit_name or None,
"evidence": evidence,
})
return passed, rejected