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