Spaces:
Sleeping
Sleeping
| """ | |
| 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", | |
| } | |