NEKReport/precheck/registry.py

266 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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,
),
# ... можно добавить остальные поля по мере необходимости
]