feat(Ф1): контракты стадий + parse_multipart + store_document

This commit is contained in:
2026-06-28 09:20:36 +04:00
parent 48d4d29493
commit a05a9691a0
2 changed files with 156 additions and 33 deletions
+84
View File
@@ -0,0 +1,84 @@
"""Контракты стадий пайплайна — 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)."""
return cls(
status=row.get("status", "error"),
element_count=0, # вычисляется отдельно
elements=row.get("elements_json", []),
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 GroupingResult:
"""Выход grouping.py — результат группировки по контрактам."""
contract_number: str # нормализованный номер договора
counterparty: Optional[str] = None
documents: list = field(default_factory=list) # [{doc_id, filename, doc_type, ...}]
unresolved: list = field(default_factory=list) # документы без группы
@classmethod
def from_row(cls, row: dict):
"""Из строки БД или вычисленной группы."""
return cls(
contract_number=row.get("contract_number", ""),
counterparty=row.get("counterparty"),
documents=row.get("documents", []),
unresolved=row.get("unresolved", []),
)
@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", ""),
)