feat(#190): метрики качества первички (AURORA-1759) #229

Merged
aep merged 45 commits from feat/190-ocr-metrics into main 2026-11-07 07:50:00 +03:00
2 changed files with 75 additions and 8 deletions
Showing only changes of commit 5f1001c808 - Show all commits

View File

@ -10,7 +10,7 @@
Объекты строк, значений и нарушений читаются атрибутами без привязки к сессии: отсутствующий атрибут даёт `None`.
"""
from collections.abc import Iterable, Sequence
from collections.abc import Iterable, Mapping, Sequence
from datetime import date, datetime
from decimal import Decimal
from enum import Enum
@ -37,22 +37,33 @@ _VALUE_FIELDS = (
_ISSUE_FIELDS = ("kind", "code", "severity", "source", "field", "relation", "codes", "line_ids")
def build_intake_payload(
def build_intake_payload( # noqa: PLR0913 - сигнатура снимка фиксирована (design §4.1)
document: Document,
*,
lines: Sequence[Any] | None = None,
values: Sequence[Any] | None = None,
issues: Iterable[Any] = (),
osv_warnings: Iterable[str] = (),
header_source: Mapping[str, Any] | None = None,
osv_row_count: int | None = None,
) -> dict[str, Any]:
"""Снимок «как распознано» для `quality_intake_fact.payload` (§14.1, design §4.1).
`BALANCE` / `INCOME`: `{"header", "lines", "values", "issues"}`. `OSV`: `{"osv_warnings"}` без `lines` и `values`.
`BALANCE` / `INCOME`: `{"header", "lines", "values", "issues"}`; при переданном `header_source` (сырой текст и
уверенность по полям шапки, `financial_statement_meta.header_source`) он кладётся в `header["header_source"]`.
`OSV`: `{"osv_warnings"}` без `lines` и `values`; при переданном `osv_row_count` добавляется `row_count`.
Без необязательных аргументов ключей `header_source` и `row_count` в payload нет.
"""
if document.document_type is DocumentType.OSV:
return {"osv_warnings": list(osv_warnings)}
payload: dict[str, Any] = {"osv_warnings": list(osv_warnings)}
if osv_row_count is not None:
payload["row_count"] = osv_row_count
return payload
header = _pick(document, _HEADER_FIELDS)
if header_source is not None:
header["header_source"] = {key: _plain_mapping(entry) for key, entry in header_source.items()}
return {
"header": _pick(document, _HEADER_FIELDS),
"header": header,
"lines": [_line(line) for line in lines or ()],
"values": [_value(value) for value in values or ()],
"issues": [_pick(issue, _ISSUE_FIELDS) for issue in issues],
@ -67,13 +78,24 @@ def build_outcome_payload( # noqa: PLR0913 - сигнатура снимка ф
issues: Iterable[Any] = (),
osv_warnings: Iterable[str] = (),
fixes: Iterable[Any] = (),
header_source: Mapping[str, Any] | None = None,
osv_row_count: int | None = None,
) -> dict[str, Any]:
"""Подтверждённый результат и правки для `quality_outcome_fact.payload` (§14.1, design §4.2).
Строение — как у снимка приёма (`build_intake_payload`), плюс `fixes`: список правок с отнесением, каждая запись
строится вызывающим (состав записи — задача отнесения правок, не этой функции).
Строение — как у снимка приёма (`build_intake_payload`, с теми же `header_source` и `osv_row_count`), плюс `fixes`:
список правок с отнесением, каждая запись строится вызывающим (состав записи — задача отнесения правок, не этой
функции).
"""
payload = build_intake_payload(document, lines=lines, values=values, issues=issues, osv_warnings=osv_warnings)
payload = build_intake_payload(
document,
lines=lines,
values=values,
issues=issues,
osv_warnings=osv_warnings,
header_source=header_source,
osv_row_count=osv_row_count,
)
payload["fixes"] = list(fixes)
return payload
@ -86,6 +108,13 @@ def _line(line: Any) -> dict[str, Any]:
}
def _plain_mapping(entry: Any) -> Any:
"""Приводит запись `header_source` (`{"text", "confidence"}`) к JSON-совместимому виду; Decimal → строка."""
if isinstance(entry, Mapping):
return {key: _plain(item) for key, item in entry.items()}
return _plain(entry)
def _value(value: Any) -> dict[str, Any]:
return _pick(value, _VALUE_FIELDS)

View File

@ -15,6 +15,8 @@ from src.models.enums import (
from src.models.files import Document
from src.models.statements import DocumentIssue, FinancialStatementLine, FinancialStatementValue
ROW_COUNT = 4_200
def make_document(document_type: DocumentType = DocumentType.BALANCE) -> Document:
return Document(
@ -125,3 +127,39 @@ def test_build_outcome_payload_osv_has_fixes_and_no_lines():
assert payload["osv_warnings"] == ["OSV_SOMETHING"]
assert payload["fixes"] == [fix]
assert "lines" not in payload and "values" not in payload
def test_build_intake_payload_includes_header_source_for_statement():
document = make_document(document_type=DocumentType.BALANCE)
header_source = {"inn": {"text": "7701234567", "confidence": Decimal("100.00")}}
payload = build_intake_payload(document, lines=[], values=[], header_source=header_source)
assert payload["header"]["header_source"] == {"inn": {"text": "7701234567", "confidence": "100.00"}}
def test_build_outcome_payload_includes_header_source_for_statement():
document = make_document(document_type=DocumentType.INCOME)
header_source = {"okpo": {"text": "12345678", "confidence": None}}
payload = build_outcome_payload(document, lines=[], values=[], fixes=[], header_source=header_source)
assert payload["header"]["header_source"] == {"okpo": {"text": "12345678", "confidence": None}}
def test_build_intake_payload_includes_osv_row_count():
document = make_document(document_type=DocumentType.OSV)
payload = build_intake_payload(document, osv_warnings=["OSV_SOMETHING"], osv_row_count=ROW_COUNT)
assert payload["row_count"] == ROW_COUNT
assert payload["osv_warnings"] == ["OSV_SOMETHING"]
def test_build_outcome_payload_includes_osv_row_count():
document = make_document(document_type=DocumentType.OSV)
payload = build_outcome_payload(document, osv_warnings=[], fixes=[], osv_row_count=0)
assert payload["row_count"] == 0
def test_header_source_and_row_count_absent_when_kwargs_omitted():
balance = build_intake_payload(make_document(document_type=DocumentType.BALANCE), lines=[], values=[])
assert "header_source" not in balance["header"]
osv = build_intake_payload(make_document(document_type=DocumentType.OSV), osv_warnings=[])
assert "row_count" not in osv
outcome_osv = build_outcome_payload(make_document(document_type=DocumentType.OSV), osv_warnings=[], fixes=[])
assert "row_count" not in outcome_osv