kcsc-mcp / src /qa_judgment.py
nicefree19's picture
Upload 105 files
0ee3c92 verified
Raw
History Blame Contribute Delete
12.4 kB
"""
KCSC QA Judgment engine (rule-based, domain-knowledge driven).
Produces draft judgments and missing-condition lists for construction-review
queries. Used by:
* `generate_qa_candidates.py` (offline evaluation against the verified golden set)
* `mcp_server.py` `qa_judgment` tool (live MCP queries)
Design rule (per `docs/qa_eval_plan.md`):
This module MUST NOT read `expected.codes`, `expected.judgment`, or
`expected.missing_conditions` from the golden set. Rules are written as a
codification of standard Korean structural-engineering review practice
(checklists a senior reviewer would intuit from work_type / check_item /
member alone, plus measurement-style terms found in retrieved KDS/KCS text).
The engine is intentionally conservative: it never emits `pass` or `fail`.
Those labels require explicit numerical/spec input which is rarely present in
free-text review queries; the safe default is `conditional_pass` or
`needs_more_info`. A future LLM-backed engine can extend this to the full
4-label space.
"""
from __future__ import annotations
import re
from typing import Any, Iterable
# ---------------------------------------------------------------------------
# Judgment label heuristics (carried over from generate_qa_candidates.py)
# ---------------------------------------------------------------------------
CONCRETE_NUMBER_PATTERN = re.compile(r"\d+(\.\d+)?\s*(mpa|kn|mm|m\b|%|Β°c|c)", re.I)
CONDITIONAL_PASS_SIGNALS: tuple[str, ...] = (
"κ²€μΈ‘", "ν’ˆμ§ˆ 확인", "ν’ˆμ§ˆκ΄€λ¦¬", "ν’ˆμ§ˆ νŒμ •", "ν’ˆμ§ˆ 검사",
"곡톡 확인사항", "μ‹œκ³΅ μ „ κ²€ν† ", "μ‹œκ³΅ 및 계츑", "μž¬ν•˜μ‹œν—˜",
"μ‹œν—˜μ‘°μ •", "ν’ˆμ§ˆμ‹œν—˜", "κ°€μ„€ κ³„νš", "좔락방지", "μ•ˆμ „μ‹œμ„€",
"μΌλ°˜μ‚¬ν•­", "곡μž₯μ œμž‘", "κ³ μž₯λ ₯볼트",
)
NEEDS_MORE_INFO_SIGNALS: tuple[str, ...] = (
"μ ν•©ν•œμ§€", "μΆ©λΆ„ν•œμ§€", "νŒλ‹¨", "섀계 κ²€ν† ", "섀계기쀀",
"적용 μ—¬λΆ€", "μ§€μ§„ν•˜μ€‘", "λ‚΄μ§„", "ν•œκ³„μƒνƒœ", "μ ‘ν•©λΆ€",
"μ§€μ§€λ ₯", "배합강도", "μ•ˆμ „ν•œμ§€",
)
# ---------------------------------------------------------------------------
# Missing-condition taxonomy
# ---------------------------------------------------------------------------
# Each entry: (required_keywords_tuple, conditions). The first rule whose
# keywords ALL appear in the case text wins. Order from most specific to most
# general. Conditions reflect typical engineering-review checklists.
# ---------------------------------------------------------------------------
SPEC_TERM_RULES: tuple[tuple[tuple[str, ...], list[str]], ...] = (
# ── Concrete (specific work types first) ─────────────────────────────
(("맀슀", "콘크리트"), ["λΆ€μž¬ 치수", "타섀 μ˜¨λ„", "양생 κ³„νš", "κ· μ—΄ μ œμ–΄ κ³„νš"]),
(("팽창", "콘크리트"), ["팽창재 μ’…λ₯˜", "λ°°ν•© 쑰건", "양생 쑰건"]),
(("μˆ˜μ€‘", "콘크리트"), ["타섀 방법", "μˆ˜μ‹¬", "μœ μ†", "λ°°ν•© 쑰건"]),
(("일반콘크리트", "타섀"), ["λ°°ν•©ν‘œ", "μŠ¬λŸΌν”„", "κ³΅κΈ°λŸ‰", "타섀 μ˜¨λ„"]),
(("타섀", "ν’ˆμ§ˆ"), ["λ°°ν•©ν‘œ", "μŠ¬λŸΌν”„", "κ³΅κΈ°λŸ‰", "타섀 μ˜¨λ„"]),
(("철근곡사", "λ°°κ·Ό"), ["μ² κ·Ό 규격", "λ°°κ·Ό 간격", "피볡 λ‘κ»˜", "도면 μš”κ΅¬μ‚¬ν•­"]),
(("철근곡사", "κ²€μΈ‘"), ["μ² κ·Ό 규격", "λ°°κ·Ό 간격", "피볡 λ‘κ»˜", "도면 μš”κ΅¬μ‚¬ν•­"]),
(("μ •μ°©", "μ² κ·Ό"), ["λΆ€μž¬ μ’…λ₯˜", "μ² κ·Ό 지름", "콘크리트 강도", "μ •μ°© μœ„μΉ˜"]),
(("동바리",), ["동바리 높이", "ν•˜μ€‘ 쑰건", "μ„€μΉ˜ 간격", "해체 μ‹œκΈ°"]),
(("κ±°ν‘Έμ§‘",), ["동바리 높이", "ν•˜μ€‘ 쑰건", "μ„€μΉ˜ 간격", "해체 μ‹œκΈ°"]),
(("μ½˜ν¬λ¦¬νŠΈκ³΅μ‚¬", "곡톡"), ["곡사 μ’…λ₯˜", "적용 μ‹œλ°©μ„œ", "ν’ˆμ§ˆ κΈ°μ€€"]),
# ── Steel ────────────────────────────────────────────────────────────
(("강ꡬ쑰곡사", "일반"), ["곡사 λ²”μœ„", "적용 재료", "μ œμž‘ 및 μ„€μΉ˜ κ³„νš"]),
(("곡μž₯μ œμž‘",), ["μ œμž‘ 도면", "재료 μ„±μ μ„œ", "μ œμž‘ ν—ˆμš©μ˜€μ°¨"]),
(("κ°•μž¬",), ["κ°•μž¬ 규격", "μ„±μ μ„œ", "검사 ν•­λͺ©"]),
(("ν•œκ³„μƒνƒœ",), ["λΆ€μž¬ μ’…λ₯˜", "ν•˜μ€‘ 쑰건", "섀계 방법"]),
(("κ³ μž₯λ ₯볼트",), ["볼트 λ“±κΈ‰", "체결 μΆ•λ ₯", "검사 방법"]),
(("볼트", "μ ‘ν•©"), ["볼트 μ’…λ₯˜", "μ ‘ν•© ν˜•μ‹", "체결 방법", "검사 κΈ°μ€€"]),
(("μš©μ ‘",), ["μš©μ ‘ μ’…λ₯˜", "μš©μ ‘ μ ˆμ°¨μ„œ", "검사 λ“±κΈ‰", "λΉ„νŒŒκ΄΄κ²€μ‚¬ μ—¬λΆ€"]),
# ── Ground / Foundation ──────────────────────────────────────────────
(("μ§€λ°˜μ‘°μ‚¬",), ["쑰사 μœ„μΉ˜", "μ‹œμΆ” μˆ˜λŸ‰", "μ‹œν—˜ κ²°κ³Ό", "μ§€μΈ΅ 쑰건"]),
(("흙막이", "ꡬ쑰"), ["적용 μ‹œμ„€", "μ§€λ°˜μ •μˆ˜", "ν•˜μ€‘ μ‘°ν•©", "κ΅΄μ°© 단계"]),
(("흙막이", "μ‹œκ³΅"), ["계츑 κ³„νš", "관리 κΈ°μ€€μΉ˜", "μ‹œκ³΅ 단계"]),
(("흙막이",), ["κ΅΄μ°© 깊이", "μ§€ν•˜μˆ˜μœ„", "흙막이 ν˜•μ‹", "μ£Όλ³€ ꡬ쑰물"]),
(("μ–•μ€κΈ°μ΄ˆ",), ["μ§€μ§€μΈ΅", "ν—ˆμš©μ§€μ§€λ ₯", "기초 ν˜•μ‹", "μΉ¨ν•˜ κ²€ν† "]),
# Pile QC judgment uses test results, not the test method itself β€” order matters
(("말뚝", "ν’ˆμ§ˆ"), ["μ‹œν—˜ κ²°κ³Ό", "μΉ¨ν•˜ κΈ°μ€€", "섀계 μ§€μ§€λ ₯"]),
(("말뚝", "μž¬ν•˜μ‹œν—˜"), ["μ‹œν—˜ 방법", "ν•˜μ€‘ 단계", "μΉ¨ν•˜λŸ‰", "νŒμ • κΈ°μ€€"]),
(("μ—°μ•½μ§€λ°˜",), ["κ°œλŸ‰ 곡법", "μ§€λ°˜ 쑰건", "ν’ˆμ§ˆ 관리 ν•­λͺ©"]),
# ── Quality / Tests ──────────────────────────────────────────────────
(("ν’ˆμ§ˆκ΄€λ¦¬κ³„νš",), ["곡사 규λͺ¨", "μ‹œν—˜ κ³„νš", "ν’ˆμ§ˆκ΄€λ¦¬ 쑰직"]),
(("μƒμˆ˜λ„",), ["κ΄€μ’…", "μ‹œν—˜ μ••λ ₯", "μ‹œν—˜ μ‹œκ°„", "λˆ„μˆ˜ μ—¬λΆ€"]),
(("μ‹œν—˜μ‘°μ •",), ["μ‹œν—˜ ν•­λͺ©", "μ„±λŠ₯ κΈ°μ€€", "μΈ‘μ • κ²°κ³Ό"]),
(("λ†μ—…μƒμ‚°κΈ°λ°˜",), ["μ‹œμ„€ μ’…λ₯˜", "계츑 ν•­λͺ©", "관리 κΈ°μ€€"]),
(("ν’ˆμ§ˆμ‹œν—˜",), ["μ‹œν—˜ μ’…λͺ©", "λΉˆλ„", "곡쒅", "μ‹œλ£Œ 채취 방법"]),
(("μ‹œν—˜", "κΈ°μ€€"), ["μ‹œν—˜ ν•­λͺ©", "재료 μ’…λ₯˜", "적용 곡쒅"]),
# ── Safety / Temporary works ─────────────────────────────────────────
(("μ•ˆμ „κ΄€λ¦¬κ³„νš",), ["곡사 규λͺ¨", "μœ„ν—˜ μž‘μ—…", "μ•ˆμ „κ΄€λ¦¬ 쑰직", "점검 κ³„νš"]),
(("ν•˜μˆ˜κΈ‰",), ["ν•˜μˆ˜κΈ‰μΈ μž‘μ—… λ²”μœ„", "μœ„ν—˜μ„±ν‰κ°€", "ꡐ윑 및 점검 기둝"]),
(("가섀곡사",), ["κ°€μ„€ μ’…λ₯˜", "ν•˜μ€‘ 쑰건", "μ„€μΉ˜ μœ„μΉ˜"]),
(("ν˜„μž₯κ°€μ„€μ‹œμ„€λ¬Ό",), ["μ„€λΉ„ μ’…λ₯˜", "μš©λŸ‰", "배치 κ³„νš", "μ•ˆμ „ 쑰치"]),
(("좔락방지",), ["μ‹œμ„€ μ’…λ₯˜", "μ„€μΉ˜ μœ„μΉ˜", "μž‘μ—… 높이"]),
(("μ•ˆμ „μ‹œμ„€", "μ „λ¬Έμ‹œλ°©"), ["μ‹œμ„€ ν˜•μ‹", "μ„€μΉ˜ ꡬ간", "κ²€μΈ‘ ν•­λͺ©"]),
(("철도", "μ•ˆμ „"), ["μž‘μ—… μ’…λ₯˜", "철도 μš΄ν–‰ 영ν–₯", "μ•ˆμ „ 쑰치"]),
# ── Seismic ──────────────────────────────────────────────────────────
(("내진섀계", "적용"), ["ꡬ쑰물 μ’…λ₯˜", "λ‚΄μ§„λ“±κΈ‰", "μ§€μ—­κ³„μˆ˜", "μ€‘μš”λ„"]),
(("내진섀계", "μ§€μ§„ν•˜μ€‘"), ["μ§€λ°˜λΆ„λ₯˜", "μœ„ν—˜λ„", "μ‘λ‹΅μˆ˜μ •κ³„μˆ˜", "μ€‘μš”λ„κ³„μˆ˜"]),
(("콘크리트ꡬ쑰", "내진상세"), ["λΆ€μž¬ μ’…λ₯˜", "내진상세 λ“±κΈ‰", "μ² κ·Ό λ°°κ·Ό", "μ—°μ„± μš”κ΅¬"]),
(("강ꡬ쑰", "λ‚΄μ§„"), ["λ‚΄μ§„μ‹œμŠ€ν…œ", "μ ‘ν•© ν˜•μ‹", "ν•˜μ€‘ μ‘°ν•©", "μ—°μ„± μš”κ΅¬"]),
(("합성ꡬ쑰", "λ‚΄μ§„"), ["ν•©μ„±λΆ€μž¬ ν˜•μ‹", "λ‚΄μ§„μ‹œμŠ€ν…œ", "μ ‘ν•© 상세", "ν•˜μ€‘ 쑰건"]),
(("기초", "λ‚΄μ§„"), ["기초 ν˜•μ‹", "μ§€λ°˜λΆ„λ₯˜", "앑상화 μ—¬λΆ€", "상뢀ꡬ쑰 반λ ₯"]),
(("λΉ„νƒˆλ©΄", "λ‚΄μ§„"), ["λΉ„νƒˆλ©΄ 높이", "μ§€λ°˜μ •μˆ˜", "μ§€μ§„κ³„μˆ˜", "배수 쑰건"]),
# ── Composite structures ─────────────────────────────────────────────
(("ν•©μ„±μ½˜ν¬λ¦¬νŠΈ", "전단"), ["λΆ€μž¬ ν˜•μ‹", "μ—°κ²°μž¬ 상세", "ν•˜μ€‘ 쑰건", "재료 강도"]),
# Steel-composite QC inspection precedes generic composite-construction rule
(("κ°•ν•©μ„±", "검사"), ["μ‹œκ³΅ 단계", "검사 ν•­λͺ©", "λΆ€μž¬ ν˜•μ‹"]),
(("합성ꡬ쑰", "μ‹œκ³΅"), ["합성ꡬ쑰 ν˜•μ‹", "타섀 κ³„νš", "ν’ˆμ§ˆμ‹œν—˜ ν•­λͺ©"]),
(("합성보",), ["합성보 단면", "μ „λ‹¨μ—°κ²°μž¬", "ν•˜μ€‘ μ‘°ν•©", "μ‹œκ³΅ 단계"]),
(("ν•©μ„±κΈ°λ‘₯",), ["λΆ€μž¬ ν˜•μ‹", "μΆ•λ ₯", "λͺ¨λ©˜νŠΈ", "재료 강도"]),
(("κ°•-콘크리트", "μ ‘ν•©"), ["μ ‘ν•©λΆ€ 상세", "ν•˜μ€‘ 전달 경둜", "재료 강도"]),
(("강합성ꡐ",), ["κ΅λŸ‰ ν˜•μ‹", "κ²½κ°„", "ν•˜μ€‘ 쑰건", "μ‹œκ³΅ 단계"]),
)
# Domain default fallback (used when no SPEC_TERM_RULES matches).
DOMAIN_DEFAULT_MISSING: dict[str, list[str]] = {
"concrete_rebar_formwork": ["λΆ€μž¬ μ’…λ₯˜", "재료 강도", "μ‹œκ³΅ μœ„μΉ˜", "도면 μš”κ΅¬μ‚¬ν•­"],
"ground_foundation_earth_retaining": ["μ§€λ°˜ 쑰건", "ν•˜μ€‘ 쑰건", "μ‹œκ³΅ 단계", "계츑 κ²°κ³Ό"],
"steel_connection_bolts": ["λΆ€μž¬ 규격", "μ ‘ν•© ν˜•μ‹", "재료 λ“±κΈ‰", "검사 κΈ°μ€€"],
"quality_test_inspection_tolerance": ["μ‹œν—˜ ν•­λͺ©", "μ‹œν—˜ λΉˆλ„", "ν—ˆμš© κΈ°μ€€", "검사 기둝"],
"safety_temporary_construction_management": ["ν˜„μž₯ 쑰건", "μœ„ν—˜ μš”μΈ", "관리 κ³„νš", "점검 기둝"],
"seismic_design_detailing": ["λ‚΄μ§„ λ“±κΈ‰", "μ§€λ°˜ 쑰건", "ν•˜μ€‘ μ‚°μ • 쑰건", "상세 도면"],
"composite_structure_criteria": ["λΆ€μž¬ ν˜•μ‹", "재료 강도", "μ—°κ²° 상세", "ν•˜μ€‘ 쑰건"],
}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def build_case_text(case: dict[str, Any]) -> str:
"""Concatenate query + structured field values for keyword matching."""
structured = case.get("input", {}).get("structured", {})
parts = [case.get("input", {}).get("query", "")]
parts.extend(str(v) for v in structured.values() if v)
return " ".join(parts)
def has_concrete_conditions(case: dict[str, Any]) -> bool:
"""Detect that the user actually supplied measurable conditions."""
structured = case.get("input", {}).get("structured", {})
if structured.get("conditions"):
return True
query = case.get("input", {}).get("query", "")
return bool(CONCRETE_NUMBER_PATTERN.search(query))
def infer_judgment(case: dict[str, Any]) -> str:
text = build_case_text(case)
if has_concrete_conditions(case):
return "conditional_pass"
if any(sig in text for sig in NEEDS_MORE_INFO_SIGNALS):
return "needs_more_info"
if any(sig in text for sig in CONDITIONAL_PASS_SIGNALS):
return "conditional_pass"
return "needs_more_info"
def _all_in(text: str, terms: Iterable[str]) -> bool:
return all(term in text for term in terms)
def infer_missing_conditions(case: dict[str, Any]) -> list[str]:
"""Return up to 4 missing conditions following review-checklist rules."""
text = build_case_text(case)
for terms, conditions in SPEC_TERM_RULES:
if _all_in(text, terms):
return list(conditions)
structured = case.get("input", {}).get("structured", {})
work_type = structured.get("work_type")
check_item = structured.get("check_item")
inferred: list[str] = []
if work_type:
inferred.append(f"{work_type} 적용 λ²”μœ„")
if check_item:
inferred.append(f"{check_item} νŒμ • κΈ°μ€€")
inferred.extend(
DOMAIN_DEFAULT_MISSING.get(case.get("domain"), ["ν˜„μž₯ 쑰건", "μ„€κ³„λ„μ„œ μš”κ΅¬μ‚¬ν•­"])
)
deduped: list[str] = []
for value in inferred:
if value and value not in deduped:
deduped.append(value)
return deduped[:4]
def judge_case(case: dict[str, Any]) -> dict[str, Any]:
"""Produce the full judgment payload for a case."""
return {
"judgment": infer_judgment(case),
"missing_conditions": infer_missing_conditions(case),
"generator": "rule_based_taxonomy_v2",
}