104 lines
4.1 KiB
Python
104 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
import re
|
|
from typing import Any
|
|
|
|
from civilplan_mcp.db.bootstrap import load_json_data
|
|
from civilplan_mcp.models import ProjectDomain
|
|
from civilplan_mcp.tools._base import wrap_response
|
|
|
|
|
|
DOMAIN_KEYWORDS = {
|
|
ProjectDomain.건축: ["건축", "청사", "센터", "병원", "학교", "복지관", "신축"],
|
|
ProjectDomain.토목_도로: ["도로", "소로", "중로", "대로", "포장", "진입도로"],
|
|
ProjectDomain.토목_상하수도: ["상수도", "하수도", "오수", "우수", "정수장"],
|
|
ProjectDomain.토목_하천: ["하천", "제방", "호안", "배수"],
|
|
ProjectDomain.조경: ["조경", "공원", "녹지", "식재", "수목", "정원"],
|
|
}
|
|
|
|
TERRAIN_MAP = {
|
|
"평지": ("평지", 1.0),
|
|
"구릉": ("구릉", 1.4),
|
|
"둔턱": ("구릉", 1.4),
|
|
"산지": ("산지", 2.0),
|
|
}
|
|
|
|
|
|
def _match_number(pattern: str, description: str) -> float | None:
|
|
matched = re.search(pattern, description, flags=re.IGNORECASE)
|
|
return float(matched.group(1)) if matched else None
|
|
|
|
|
|
def _detect_domain(description: str) -> tuple[ProjectDomain, list[str]]:
|
|
detected: list[ProjectDomain] = []
|
|
for domain, keywords in DOMAIN_KEYWORDS.items():
|
|
if any(keyword in description for keyword in keywords):
|
|
detected.append(domain)
|
|
|
|
if not detected:
|
|
return ProjectDomain.복합, []
|
|
if ProjectDomain.토목_도로 in detected and ProjectDomain.토목_상하수도 in detected and len(detected) == 2:
|
|
return ProjectDomain.토목_도로, [ProjectDomain.토목_상하수도.value]
|
|
if len(detected) == 1:
|
|
return detected[0], []
|
|
return ProjectDomain.복합, [item.value for item in detected]
|
|
|
|
|
|
def parse_project(*, description: str) -> dict[str, Any]:
|
|
region_factors = load_json_data("region_factors.json")
|
|
domain, sub_domains = _detect_domain(description)
|
|
|
|
road_length = _match_number(r"L\s*=\s*([0-9]+(?:\.[0-9]+)?)m", description)
|
|
road_width = _match_number(r"B\s*=\s*([0-9]+(?:\.[0-9]+)?)m", description)
|
|
lanes = int(_match_number(r"([0-9]+)\s*차선", description) or 2)
|
|
years = re.search(r"(20[0-9]{2})\s*[~\-]\s*(20[0-9]{2})", description)
|
|
|
|
terrain = "평지"
|
|
terrain_factor = 1.0
|
|
for keyword, (terrain_name, factor) in TERRAIN_MAP.items():
|
|
if keyword in description:
|
|
terrain = terrain_name
|
|
terrain_factor = factor
|
|
break
|
|
|
|
region = next((name for name in region_factors if name in description), "경기도")
|
|
year_start = int(years.group(1)) if years else datetime.now().year
|
|
year_end = int(years.group(2)) if years else year_start
|
|
utilities = [utility for utility in ("상수도", "하수도") if utility in description]
|
|
pavement = "아스콘" if "아스콘" in description else "콘크리트" if "콘크리트" in description else None
|
|
road_class = "소로" if "소로" in description else "중로" if "중로" in description else "대로" if "대로" in description else None
|
|
|
|
result = {
|
|
"project_id": f"PRJ-{datetime.now():%Y%m%d}-001",
|
|
"domain": domain.value,
|
|
"sub_domains": sub_domains,
|
|
"project_type": [item for item in ["도로", "건축", "상수도", "하수도"] if item in description] or [domain.value],
|
|
"road": {
|
|
"class": road_class,
|
|
"length_m": road_length,
|
|
"width_m": road_width,
|
|
"lanes": lanes,
|
|
"pavement": pavement,
|
|
},
|
|
"terrain": terrain,
|
|
"terrain_factor": terrain_factor,
|
|
"region": region,
|
|
"region_factor": region_factors[region]["factor"],
|
|
"year_start": year_start,
|
|
"year_end": year_end,
|
|
"duration_years": year_end - year_start + 1,
|
|
"utilities": utilities,
|
|
"has_farmland": None,
|
|
"has_forest": None,
|
|
"has_river": None,
|
|
"parsed_confidence": 0.92 if road_length and road_width else 0.72,
|
|
"ambiguities": [],
|
|
"warnings": [],
|
|
}
|
|
|
|
if sub_domains:
|
|
result["domain_warning"] = f"복합 사업 감지: {', '.join(sub_domains)}"
|
|
|
|
return wrap_response(result, domain)
|