Spaces:
Runtime error
Runtime error
File size: 4,004 Bytes
aad7814 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | """Multi-stage RICS L3 note routing: explicit prefix β element label β keywords β semantic fallback.
Priority 1 β explicit section tokens at line start (``G1,``, ``D9:``, ``D4 -``).
Priority 1b β surveyor element labels (``Main roof:``, ``Rainwater fittings:``).
Priority 2 β high-confidence domain keyword tiers (full canonical lexicon).
Priority 3 β general keyword patterns, then embedding/RAG when enabled.
"""
from __future__ import annotations
import re
from collections import defaultdict
from backend.core.notes_element_labels import ELEMENT_LABEL_ROUTES
from backend.core.notes_high_confidence_lexicon import compile_high_confidence_patterns
from backend.core.rics_canonical_l3 import valid_leaf_section_ids
UNASSIGNED = "UNASSIGNED"
# Priority 1: canonical leaf id at line start, optional separator, remainder is body.
_EXPLICIT_SECTION_PREFIX = re.compile(
r"^\s*(?:section\s+)?([A-N]\d{1,2})\s*(?:[:,;\-ββ\.]\s*|\s+)(.+)$",
re.IGNORECASE | re.DOTALL,
)
_ELEMENT_LABEL_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
(
section_id,
re.compile(rf"^\s*({label})\s*:\s*(.+)$", re.IGNORECASE | re.DOTALL),
)
for section_id, label in ELEMENT_LABEL_ROUTES
]
# Priority 2: full canonical lexicon (order-sensitive; see notes_high_confidence_lexicon.py).
_HIGH_CONFIDENCE_PATTERNS = compile_high_confidence_patterns()
RAG_CLASSIFICATION_GUIDANCE = (
"You are a strict RICS Level 3 surveyor assistant. Allocate the following note "
"to exactly one of the 54 canonical sub-sections. If the text describes a building "
"element (like gutters), do not assign it to the surrounding element (like main walls); "
"assign it to its exact dedicated section (D3)."
)
def parse_explicit_section_prefix(line: str) -> tuple[str, str] | None:
"""Return ``(section_id, body)`` when the line starts with a canonical leaf token."""
text = (line or "").strip()
if len(text) < 3:
return None
match = _EXPLICIT_SECTION_PREFIX.match(text)
if not match:
return None
section_id = match.group(1).upper()
body = match.group(2).strip()
allowed = valid_leaf_section_ids()
if section_id in allowed and body:
return section_id, body
return None
def parse_element_label_prefix(line: str) -> tuple[str, str] | None:
"""Return ``(section_id, full_line)`` when the line opens with a known element label."""
text = (line or "").strip()
if len(text) < 3 or ":" not in text:
return None
allowed = valid_leaf_section_ids()
for section_id, pattern in _ELEMENT_LABEL_PATTERNS:
match = pattern.match(text)
if not match:
continue
body = match.group(2).strip()
if body and section_id.upper() in allowed:
return section_id.upper(), text
return None
def classify_high_confidence_keywords(text: str) -> tuple[str, float]:
"""Priority-2 keyword tier; returns ``UNASSIGNED`` when no rule fires."""
line = (text or "").strip()
if len(line) < 3:
return UNASSIGNED, 0.0
valid = valid_leaf_section_ids()
for section_id, pattern in _HIGH_CONFIDENCE_PATTERNS:
if pattern.search(line):
sid = section_id.upper()
if sid in valid:
return sid, 1.0
return UNASSIGNED, 0.0
def route_lines_to_l3_sections(lines: list[str]) -> tuple[dict[str, list[str]], list[str]]:
"""Route note lines into L3 section buckets using the full cascade."""
from backend.core.notes_keyword_router import classify_note_cascade
routed: dict[str, list[str]] = defaultdict(list)
unmatched: list[str] = []
for raw in lines:
text = (raw or "").strip()
if len(text) < 3:
continue
section_id, _, observation = classify_note_cascade(text)
if section_id == UNASSIGNED:
unmatched.append(observation)
else:
routed[section_id].append(observation)
return dict(routed), unmatched
|