# AI Ping teaching lab: fictional data, no network or API calls.
# Claude Sonnet 5.5 draft + recorded revision, 2026-10-28.
# Editor combined the patch and added held-out cases and this runner.
from typing import Callable, Optional

DOCS = [
    {"id": "D01", "access": "public", "title": "운영시간", "body": "매일 10:00~18:00.", "tags": ("운영", "시간표")},
    {"id": "D02", "access": "public", "title": "회의실", "body": "14일 전부터 예약, 최대 6명.", "tags": ("회의실", "예약")},
    {"id": "D03", "access": "public", "title": "도서 대출", "body": "2권, 7일.", "tags": ("대출", "도서")},
    {"id": "D04", "access": "public", "title": "인쇄", "body": "흑백 한 장 100원.", "tags": ("인쇄", "프린트")},
    {"id": "D05", "access": "public", "title": "음식", "body": "라운지에서만 허용.", "tags": ("음식", "식사")},
    {"id": "D06", "access": "staff", "title": "직원용 테스트 기록", "body": "DEMO-ONLY", "tags": ("직원", "출입")},
]

# 문서 내용과 분리된 표현 확장 표. 문서 ID별 일반적인 다른 말투만 담는다.
ALIASES = {
    "D01": ("몇 시", "문을 열", "개관", "폐관", "오픈"),
    "D02": ("모임", "스터디", "대관"),
    "D03": ("책을", "빌려", "대여", "반납"),
    "D04": ("복사", "출력", "프린터"),
    "D05": ("간식", "음료", "먹어도", "취식"),
}

DEV_CASES = [
    ("Q01", "운영시간 알려줘", "D01"),
    ("Q02", "몇 시에 문을 열어?", "D01"),
    ("Q03", "회의실 예약은?", "D02"),
    ("Q04", "모임할 방을 빌리고 싶어", "D02"),
    ("Q05", "도서 대출 기간", "D03"),
    ("Q06", "책을 빌리려면?", "D03"),
    ("Q07", "주차장 있나요?", None),
    ("Q08", "직원 출입 정보", None),
]


def _visible_docs():
    # 접근 권한 검사는 점수 계산 전에 적용한다.
    return sorted((d for d in DOCS if d["access"] == "public"), key=lambda d: d["id"])


def _best_doc(question: str, terms_for: Callable[[dict], tuple]) -> Optional[str]:
    best_id, best_score = None, 0
    for doc in _visible_docs():
        hits = sum(1 for term in set(terms_for(doc)) if term in question)
        if hits > best_score:  # 엄격히 클 때만 교체 -> 동률은 문서 ID순
            best_id, best_score = doc["id"], hits
    return best_id


def search_baseline(question: str) -> Optional[str]:
    return _best_doc(question, lambda d: d["tags"])


# 이 도서관 자료에 없는 시설을 가리키는 말. 질문에 있으면 별칭이 맞아도 답하지 않고 보류한다.
OUT_OF_SCOPE_FACILITIES = (
    "카페", "커피숍", "식당", "매점", "편의점", "주차", "서점", "은행",
    "병원", "약국", "수영장", "체육관", "박물관", "영화관", "우체국",
)


# 직원 등 내부 대상을 가리키는 말. 공개 문서는 일반 이용자용이므로 이런 대상의 질문은 보류한다.
STAFF_AUDIENCE_MARKERS = ("직원", "스태프", "관계자", "관리자")


def search_expanded(question: str) -> Optional[str]:
    if any(facility in question for facility in OUT_OF_SCOPE_FACILITIES):
        return None
    if any(marker in question for marker in STAFF_AUDIENCE_MARKERS):
        return None
    return _best_doc(question, lambda d: d["tags"] + ALIASES.get(d["id"], ()))

def score(cases, search):
    failed_ids = [qid for qid, question, expected in cases if search(question) != expected]
    return len(cases) - len(failed_ids), len(cases), failed_ids


# Initially held out; these cases became regression tests after the third prompt.
HOLDOUT_CASES = [
    ("H01", "폐관 시각은?", "D01"),
    ("H02", "스터디 공간 신청", "D02"),
    ("H03", "출력 한 장 가격?", "D04"),
    ("H04", "직원 전용 출입 몇 시예요?", None),
]
FRESH_CASES = [
    ("F01", "프린터 가격은?", "D04"),
    ("F02", "음료 마셔도 돼요?", "D05"),
    ("F03", "은행 몇 시에 문을 열어?", None),
    ("F04", "모임 예약 최대 인원은?", "D02"),
]
DIAGNOSTIC = [("X01", "카페 몇 시에 문을 열어?", None)]

if __name__ == "__main__":
    for label, cases in (("development", DEV_CASES), ("diagnostic", DIAGNOSTIC), ("regression", HOLDOUT_CASES), ("fresh", FRESH_CASES)):
        for name, fn in (("baseline", search_baseline), ("expanded", search_expanded)):
            passed, total, failures = score(cases, fn)
            print(f"{label} {name}: {passed}/{total} failed={failures}")
