266 lines
9.5 KiB
Python
266 lines
9.5 KiB
Python
# precheck/registry.py
|
||
from dataclasses import dataclass, field
|
||
from typing import Callable, Optional, Any, List, Dict
|
||
from .extractors.weights import extract_preferred_weight_kg, extract_quantities_from_text
|
||
from .extractors.dimensions import extract_dimensions_from_text, extract_vehicle_dimensions_from_text
|
||
from .extractors.containers import extract_container_mentions
|
||
from .extractors.dangerous import extract_dangerous_phrases
|
||
|
||
@dataclass
|
||
class FieldDefinition:
|
||
name: str # ключ в JSON shipment
|
||
label: str # человекочитаемое название
|
||
type: str # "string", "number", "boolean", "array", "object"
|
||
required_by_default: bool = True # обязательно для любого типа перевозки?
|
||
category: str = "general" # для группировки в промпте
|
||
extractor: Optional[Callable[[str, Optional[Dict]], Any]] = None # функция(text, shipment=None) -> Any
|
||
validator: Optional[Callable[[Any], bool]] = None
|
||
dependencies: List[str] = field(default_factory=list) # имена полей, от которых зависит
|
||
default_value: Any = None # значение по умолчанию, если не найдено и не обязательно
|
||
|
||
def _is_non_empty_string(val: Any) -> bool:
|
||
return isinstance(val, str) and bool(val.strip())
|
||
|
||
def _is_positive_number(val: Any) -> bool:
|
||
if val is None:
|
||
return False
|
||
if isinstance(val, (int, float)):
|
||
return val > 0
|
||
return False
|
||
|
||
def _is_valid_dimensions(val: Any) -> bool:
|
||
if not isinstance(val, list):
|
||
return False
|
||
for d in val:
|
||
if not isinstance(d, dict):
|
||
return False
|
||
if not all(isinstance(d.get(k), (int, float)) and d.get(k, 0) > 0 for k in ("length_cm", "width_cm", "height_cm")):
|
||
return False
|
||
return True
|
||
|
||
def _is_valid_dangerous_goods(val: Any) -> bool:
|
||
if not isinstance(val, dict):
|
||
return False
|
||
for k in ("batteries", "gases", "liquids", "dry_ice"):
|
||
if k in val and val[k] not in (True, False, None):
|
||
return False
|
||
return True
|
||
|
||
# Простые экстракторы-обёртки для использования в реестре
|
||
def _extract_total_weight(text: str, shipment: Optional[Dict] = None) -> Optional[float]:
|
||
res = extract_preferred_weight_kg(text, shipment)
|
||
return res.get("value") if res and res.get("value") else None
|
||
|
||
def _extract_package_count(text: str, shipment: Optional[Dict] = None) -> Optional[int]:
|
||
quant = extract_quantities_from_text(text)
|
||
# берём из отдельной суммы или триплетов
|
||
cnt = quant.get("separate_sum", {}).get("package_count")
|
||
if cnt is None and quant.get("triplet_sum"):
|
||
cnt = quant["triplet_sum"].get("package_count")
|
||
return int(cnt) if cnt is not None else None
|
||
|
||
def _extract_total_volume(text: str, shipment: Optional[Dict] = None) -> Optional[float]:
|
||
quant = extract_quantities_from_text(text)
|
||
vol = quant.get("separate_sum", {}).get("total_volume_cbm")
|
||
if vol is None and quant.get("triplet_sum"):
|
||
vol = quant["triplet_sum"].get("total_volume_cbm")
|
||
return vol
|
||
|
||
def _extract_dimensions(text: str, shipment: Optional[Dict] = None) -> Optional[List[Dict]]:
|
||
dims = extract_dimensions_from_text(text)
|
||
return dims if dims else None
|
||
|
||
def _extract_vehicle_dimensions(text: str, shipment: Optional[Dict] = None) -> Optional[List[Dict]]:
|
||
vdims = extract_vehicle_dimensions_from_text(text)
|
||
return vdims if vdims else None
|
||
|
||
def _extract_dangerous_goods(text: str, shipment: Optional[Dict] = None) -> Optional[Dict]:
|
||
phrases = extract_dangerous_phrases(text)
|
||
if not phrases:
|
||
return None
|
||
# грубая классификация по ключевым словам
|
||
result = {"batteries": None, "gases": None, "liquids": None, "dry_ice": None}
|
||
text_low = text.lower()
|
||
if any(w in text_low for w in ["батаре", "batter", "lithium"]):
|
||
result["batteries"] = True
|
||
if any(w in text_low for w in ["газ", "gas", "пропан", "бутан"]):
|
||
result["gases"] = True
|
||
if any(w in text_low for w in ["жидкост", "liquid", "растворител", "краск"]):
|
||
result["liquids"] = True
|
||
if any(w in text_low for w in ["сухой лёд", "dry ice"]):
|
||
result["dry_ice"] = True
|
||
return result
|
||
|
||
# Реестр всех полей
|
||
FIELD_REGISTRY: List[FieldDefinition] = [
|
||
FieldDefinition(
|
||
name="client_name",
|
||
label="Клиент",
|
||
type="string",
|
||
required_by_default=True,
|
||
category="general",
|
||
extractor=None, # пока нет надёжного экстрактора, оставим None
|
||
validator=_is_non_empty_string
|
||
),
|
||
FieldDefinition(
|
||
name="incoterms",
|
||
label="Условия поставки Incoterms",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="general",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="cargo_ready_date",
|
||
label="Дата готовности груза",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="general",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="pickup_address",
|
||
label="Адрес забора груза",
|
||
type="string",
|
||
required_by_default=True,
|
||
category="route",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="delivery_address",
|
||
label="Адрес доставки",
|
||
type="string",
|
||
required_by_default=True,
|
||
category="route",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="cargo_value",
|
||
label="Стоимость груза",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="general",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="package_count",
|
||
label="Количество грузовых мест",
|
||
type="number",
|
||
required_by_default=True,
|
||
category="cargo",
|
||
extractor=_extract_package_count,
|
||
validator=_is_positive_number,
|
||
),
|
||
FieldDefinition(
|
||
name="total_weight_kg",
|
||
label="Вес груза (кг)",
|
||
type="number",
|
||
required_by_default=True,
|
||
category="cargo",
|
||
extractor=_extract_total_weight,
|
||
validator=_is_positive_number,
|
||
),
|
||
FieldDefinition(
|
||
name="dimensions",
|
||
label="Габариты грузовых мест",
|
||
type="array",
|
||
required_by_default=False,
|
||
category="cargo",
|
||
extractor=_extract_dimensions,
|
||
validator=_is_valid_dimensions,
|
||
),
|
||
FieldDefinition(
|
||
name="total_volume_cbm",
|
||
label="Общий объём (м³)",
|
||
type="number",
|
||
required_by_default=False,
|
||
category="cargo",
|
||
extractor=_extract_total_volume,
|
||
validator=_is_positive_number,
|
||
dependencies=["dimensions"], # может быть вычислен из размеров
|
||
),
|
||
FieldDefinition(
|
||
name="cargo_description",
|
||
label="Характер груза",
|
||
type="string",
|
||
required_by_default=True,
|
||
category="cargo",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="hs_code",
|
||
label="Код ТН ВЭД",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="cargo",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="dangerous_goods",
|
||
label="Опасные свойства",
|
||
type="object",
|
||
required_by_default=False,
|
||
category="cargo",
|
||
extractor=_extract_dangerous_goods,
|
||
validator=_is_valid_dangerous_goods,
|
||
),
|
||
FieldDefinition(
|
||
name="msds_required",
|
||
label="Требуется MSDS",
|
||
type="boolean",
|
||
required_by_default=False,
|
||
category="documents",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="dgm_report_required",
|
||
label="Требуется DGM",
|
||
type="boolean",
|
||
required_by_default=False,
|
||
category="documents",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="brand_name",
|
||
label="Бренд",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="general",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="container_type",
|
||
label="Тип контейнера",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="transport",
|
||
extractor=lambda text, sh: ", ".join(extract_container_mentions(text)) if extract_container_mentions(text) else None,
|
||
),
|
||
FieldDefinition(
|
||
name="vehicle_type",
|
||
label="Тип транспорта",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="transport",
|
||
extractor=None,
|
||
),
|
||
FieldDefinition(
|
||
name="vehicle_dimensions",
|
||
label="Габариты машины",
|
||
type="array",
|
||
required_by_default=False,
|
||
category="transport",
|
||
extractor=_extract_vehicle_dimensions,
|
||
validator=_is_valid_dimensions,
|
||
),
|
||
FieldDefinition(
|
||
name="temperature_range",
|
||
label="Температурный режим",
|
||
type="string",
|
||
required_by_default=False,
|
||
category="transport",
|
||
extractor=None,
|
||
),
|
||
# ... можно добавить остальные поля по мере необходимости
|
||
]
|