"""Контракты стадий пайплайна — frozen dataclass на выходах, dict на входах.""" from dataclasses import dataclass, field from typing import Optional from datetime import date @dataclass(frozen=True) class ParseResult: """Выход parse.py — результат парсинга документа.""" status: str # 'parsed' | 'error' element_count: int # количество извлечённых элементов elements: list # [{type, text/rows}, ...] error: Optional[str] = None # сообщение об ошибке (если status='error') @classmethod def from_row(cls, row: dict): """Из строки БД (documents).""" elements = row.get("elements_json", []) return cls( status=row.get("status", "error"), element_count=len(elements) if isinstance(elements, list) else 0, elements=elements, error=row.get("error_message"), ) @dataclass(frozen=True) class ClassifyResult: """Выход classify.py — результат классификации документа.""" doc_type: str # 'contract' | 'supplement' | 'specification' | 'garbage' | 'other' own_number: Optional[str] = None parent_number: Optional[str] = None doc_date: Optional[str] = None counterparty: Optional[str] = None @classmethod def from_llm(cls, raw: dict): """Из сырого ответа LLM.""" return cls( doc_type=raw.get("doc_type", "other"), own_number=raw.get("own_number") or None, parent_number=raw.get("parent_number") or None, doc_date=raw.get("doc_date") or None, counterparty=raw.get("counterparty") or None, ) @dataclass(frozen=True) class BatchGroupingResult: """Выход grouping.py — результат группировки всего батча.""" groups: list = field(default_factory=list) # list[GroupingResult] unresolved: list = field(default_factory=list) # документы без группы @dataclass(frozen=True) class GroupingResult: """Выход grouping.py — результат группировки по контрактам.""" contract_number: str # нормализованный номер договора counterparty: Optional[str] = None documents: list = field(default_factory=list) # [{doc_id, filename, doc_type, ...}] @classmethod def from_row(cls, row: dict): return cls( contract_number=row.get("contract_number", ""), counterparty=row.get("counterparty"), documents=row.get("documents", []), ) @dataclass(frozen=True) class CompareOp: """Одна операция сравнения (ADD/UPDATE/DELETE/UNRESOLVED).""" action: str # 'ADD' | 'UPDATE' | 'DELETE' | 'UNRESOLVED' new_row: Optional[dict] = None new_values: Optional[dict] = None target_hash: Optional[str] = None comment: str = "" @classmethod def from_llm(cls, raw: dict): """Из сырого ответа LLM.""" return cls( action=raw.get("action", "UNRESOLVED"), new_row=raw.get("new_row") or None, new_values=raw.get("new_values") or None, target_hash=raw.get("target_hash") or raw.get("target_id"), comment=raw.get("comment", ""), )