# -*- coding: utf-8 -*- """Справочник ISO-контейнеров: промпт RAG и сверка п.21 FCL (дверной проём vs габариты груза).""" from __future__ import annotations import json import os import re from typing import Any, Dict, List, Optional, Tuple _DIR = os.path.dirname(os.path.abspath(__file__)) _JSON_PATH = os.path.join(_DIR, "container_iso_reference.json") _iso_cache: Optional[Dict[str, Any]] = None def load_iso_reference() -> Dict[str, Any]: global _iso_cache if _iso_cache is not None: return _iso_cache try: with open(_JSON_PATH, "r", encoding="utf-8") as f: _iso_cache = json.load(f) except Exception: _iso_cache = {"types": {}} return _iso_cache def iso_reference_prompt_block() -> str: """Компактный блок для system prompt LLM (сверка п.21 и выбора контейнера).""" data = load_iso_reference() types = data.get("types") or {} if not types: return "" lines: List[str] = [ "СПРАВОЧНИК ISO-КОНТЕЙНЕРОВ (мм, кг, м³) — для сверки с п.21 FCL (дверной проём) и оценки объёма/полезной нагрузки. " "Не выдумывай числа; при расчётах используй только эти значения.", "Ключи 20DV / 40DV / 40HC соответствуют формулировкам в критериях (проём 20DV, 40DV, 40HC).", ] for key in ("20DV", "20HC", "20HC_PW", "40DV", "40HC", "40HC_PW", "45HC_PW_A", "45HC_PW_B"): t = types.get(key) if not isinstance(t, dict): continue door = t.get("door_opening_mm") or {} inn = t.get("internal_mm") or {} dw = door.get("width") dh = door.get("height") il = inn.get("length") iw = inn.get("width") ih = inn.get("height") vol = t.get("volume_m3") pay = t.get("payload_kg") lines.append( f"- {key} ({t.get('label', '')}): дверь {dw}×{dh} мм; " f"внутри {il}×{iw}×{ih} мм; объём ~{vol} м³; payload ~{pay} кг" ) return "\n".join(lines) def _edges_mm_from_dim(d: Dict[str, Any]) -> Optional[Tuple[float, float, float]]: """Длина/ширина/высота груза в мм (из см в shipment).""" if not isinstance(d, dict): return None def _num(v: Any) -> Optional[float]: if v is None: return None if isinstance(v, (int, float)): return float(v) s = str(v).strip().replace(" ", "").replace(",", ".") try: return float(s) except ValueError: return None l = _num(d.get("length_cm")) w = _num(d.get("width_cm")) h = _num(d.get("height_cm")) if l is None or w is None or h is None: return None return l * 10.0, w * 10.0, h * 10.0 def _box_fits_door( edges_mm: Tuple[float, float, float], door_w_mm: float, door_h_mm: float, ) -> bool: """Два наименьших ребра груза должны укладываться в ширину/высоту проёма (сортировка).""" s0, s1, s2 = sorted(edges_mm) dw, dh = sorted((door_w_mm, door_h_mm)) return s0 <= dw + 0.5 and s1 <= dh + 0.5 def _codes_from_container_type(ct: str) -> List[str]: """Из строки вида «2×40HC, 1×20DC» извлекает коды из справочника.""" if not isinstance(ct, str) or not ct.strip(): return ["20DV", "40DV", "40HC"] t = ct.upper() found: List[str] = [] def _add(size: str, kind: Optional[str]) -> None: k = (kind or "DV").upper() if size == "20": code = "20" + ("HC" if k in ("HC", "HQ") else "DV") elif size == "40": code = "40" + ("HC" if k in ("HC", "HQ") else "DV") elif size == "45": code = "45HC_PW_B" else: return if code not in found: found.append(code) for m in re.finditer( r"(\d{1,3})\s*[x×х**]\s*(20|40|45)\s*(DC|DV|GP|HC|HQ|OT|FR|RF|RH)?", t, re.IGNORECASE, ): _add(m.group(2), m.group(3)) for m in re.finditer( r"\b(20|40|45)\s*(DC|DV|GP|HC|HQ|OT|FR|RF|RH)\b", t, re.IGNORECASE, ): _add(m.group(1), m.group(2)) if not found: return ["20DV", "40DV", "40HC"] return found def door_clearance_summary(shipment: Dict[str, Any]) -> str: """ Текст для п.21: сверка габаритов мест с дверным проёмом 20DV/40DV/40HC из справочника. """ data = load_iso_reference() types = data.get("types") or {} dims = shipment.get("dimensions") if not isinstance(dims, list) or not dims: return ( "Нет габаритов грузовых мест — проверка проходимости по справочнику дверных проёмов " "(20DV 2336×2291 мм, 40DV 2336×2291 мм, 40HC 2340×2597 мм) невозможна." ) ct_raw = (shipment.get("container_type") or "").strip() codes = _codes_from_container_type(ct_raw) parts: List[str] = [] for i, d in enumerate(dims, start=1): edges = _edges_mm_from_dim(d) if not edges: parts.append(f"Место {i}: нет полных Д×Ш×В в см.") continue place_bits: List[str] = [] for code in codes: spec = types.get(code) if not isinstance(spec, dict): continue door = spec.get("door_opening_mm") or {} dw = float(door.get("width") or 0) dh = float(door.get("height") or 0) if dw <= 0 or dh <= 0: continue ok = _box_fits_door(edges, dw, dh) label = spec.get("label", code) status = "проходит" if ok else "не проходит" place_bits.append(f"{code} ({int(dw)}×{int(dh)} мм): {status}") if place_bits: parts.append(f"Место {i} (рёбра {int(min(edges))}×{int(sorted(edges)[1])}×{int(max(edges))} мм): " + "; ".join(place_bits)) else: parts.append(f"Место {i}: нет данных дверного проёма в справочнике для кодов {', '.join(codes)}.") note = ( " Оценка: два наименьших габарита сравниваются с шириной и высотой дверного проёма (ориентация коробки)." ) prefix = ( f"Справочник дверных проёмов (п.21). Контейнеры в заявке: {ct_raw or 'не указаны — проверка по 20DV/40DV/40HC'}. " ) return prefix + " ".join(parts) + note