Merge pull request 'AURORA-1734, 1735, 1864: pylib_ai_arm_statements 0.1.5' (#55) from feat/54-statements-015 into main
This commit is contained in:
commit
68724fbf01
@ -8,18 +8,18 @@
|
|||||||
старом АРМ), забрать **все** `Исходящие/<job_id>/*.json` (ИС кладёт по одному json на каждый
|
старом АРМ), забрать **все** `Исходящие/<job_id>/*.json` (ИС кладёт по одному json на каждый
|
||||||
распознанный документ), склеить и сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить
|
распознанный документ), склеить и сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), очистить
|
||||||
горячую папку (только при `SCAN_CLEANUP=true`), нормализовать в `../docs/contracts/recognition-result.schema.json`
|
горячую папку (только при `SCAN_CLEANUP=true`), нормализовать в `../docs/contracts/recognition-result.schema.json`
|
||||||
(`statementDocument` v2.0 общим кодом `arm_statements`: по одному документу на таблицу `table_structure[i]`), опубликовать `files.tabular_parsed`
|
(`statementDocument` v2.0 общим кодом `arm_statements`: по документу на таблицу `table_structure[i]`, а при смене формы внутри таблицы — на каждую часть), опубликовать `files.tabular_parsed`
|
||||||
(по умолчанию; задаётся `OPENBAO__NATS__NATS_*_SUBJECT`) (`done` / `needs_check` / `error`). Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=1200)`,
|
(по умолчанию; задаётся `OPENBAO__NATS__NATS_*_SUBJECT`) (`done` / `needs_check` / `error`). Таймаут → ошибка `TIMEOUT` по `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS=1200)`,
|
||||||
старт — запись файла во «Входящие». **Нет доступа к БД.**
|
старт — запись файла во «Входящие». **Нет доступа к БД.**
|
||||||
|
|
||||||
## Правила
|
## Правила
|
||||||
- `arm_statements` — библиотека `pylib_ai_arm_statements==0.1.1`, не публикуется в PyPI.
|
- `arm_statements` — библиотека `pylib_ai_arm_statements` (версия — пин в `api/requirements.txt`), не публикуется в PyPI.
|
||||||
`make venv` и `make install` устанавливают её из `../pylib_ai_arm_statements` (`STATEMENTS_DIR`).
|
`make venv` и `make install` устанавливают её из `../pylib_ai_arm_statements` (`STATEMENTS_DIR`).
|
||||||
- Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14), по `05-domain-rules.md`
|
- Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14), по `05-domain-rules.md`
|
||||||
§11, §11.1 и §11.9: правило, которого там нет, не реализуется, а поднимается вопросом. Шаги 2–8, словари и статусы —
|
§11, §11.1 и §11.9: правило, которого там нет, не реализуется, а поднимается вопросом. Шаги 2–8, словари и статусы —
|
||||||
в пакете `arm_statements`; в шлюзе остаются только:
|
в пакете `arm_statements`; в шлюзе остаются только:
|
||||||
- адаптер шага 1 — `api/src/normalizer/ocr/adapter.py` (таблица ИС → кандидат, `ref` — номер строки ИС);
|
- адаптер шага 1 — `api/src/normalizer/ocr/adapter.py` (таблица ИС → кандидат, `ref` — номер строки ИС);
|
||||||
- обход набора json — `api/src/normalizer/ocr/answer_set.py` (`normalize_candidate` на каждую таблицу,
|
- обход набора json — `api/src/normalizer/ocr/answer_set.py` (`split_by_form` на каждую таблицу — ББ и ОФР одной таблицей ИС, AURORA-1734 — и `normalize_candidate` на каждую часть,
|
||||||
`envelope_status`, коды `JSON_FAILED` / `TABLE_PARSE_ERROR`; Р-32/Р-33);
|
`envelope_status`, коды `JSON_FAILED` / `TABLE_PARSE_ERROR`; Р-32/Р-33);
|
||||||
- граница контракта — `to_contract` / `warning_to_contract` пакета, вызываются в `api/src/normalizer/ocr/__init__.py`
|
- граница контракта — `to_contract` / `warning_to_contract` пакета, вызываются в `api/src/normalizer/ocr/__init__.py`
|
||||||
(`OcrNormalizer`); своего конвертера и своего уплощения шапки нет, шлюз только валидирует словарь моделями
|
(`OcrNormalizer`); своего конвертера и своего уплощения шапки нет, шлюз только валидирует словарь моделями
|
||||||
|
|||||||
10
README.md
10
README.md
@ -158,8 +158,10 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
|||||||
пакета (сетка ячеек по возрастанию `row_index`, `ref` ячейки — номер строки ИС, шапка `data_ocr`, заголовок формы).
|
пакета (сетка ячеек по возрастанию `row_index`, `ref` ячейки — номер строки ИС, шапка `data_ocr`, заголовок формы).
|
||||||
Нарушенная структура таблицы (повтор `row_index` / `column_index`, нецелые индексы, `rows` не список) — исключение
|
Нарушенная структура таблицы (повтор `row_index` / `column_index`, нецелые индексы, `rows` не список) — исключение
|
||||||
адаптера, то есть `TABLE_PARSE_ERROR` всей таблицы.
|
адаптера, то есть `TABLE_PARSE_ERROR` всей таблицы.
|
||||||
- `ocr/answer_set.py` — обход набора json склейки: на каждую таблицу успешного файла зовётся `normalize_candidate`
|
- `ocr/answer_set.py` — обход набора json склейки: кандидат каждой таблицы успешного файла режется `split_by_form`
|
||||||
пакета, исключение одной таблицы не роняет остальные; статус набора — `envelope_status` пакета.
|
пакета (ББ и ОФР одной таблицей ИС — две части, AURORA-1734), на каждую часть зовётся `normalize_candidate`;
|
||||||
|
исключение одной таблицы или части не роняет остальные (`TABLE_PARSE_ERROR` с её `locator`); статус набора —
|
||||||
|
`envelope_status` пакета.
|
||||||
- `ocr/__init__.py` — `OcrNormalizer`: результат пакета переводится в модели контракта функциями `to_contract` /
|
- `ocr/__init__.py` — `OcrNormalizer`: результат пакета переводится в модели контракта функциями `to_contract` /
|
||||||
`warning_to_contract` (граница контракта; шлюз только валидирует словарь своими pydantic-моделями), версия библиотеки
|
`warning_to_contract` (граница контракта; шлюз только валидирует словарь своими pydantic-моделями), версия библиотеки
|
||||||
(`statements_version()`) идёт в `NormalizeOutcome` и в `service.statements_version` файла результата.
|
(`statements_version()`) идёт в `NormalizeOutcome` и в `service.statements_version` файла результата.
|
||||||
@ -170,7 +172,9 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
|||||||
удалены.
|
удалены.
|
||||||
|
|
||||||
Документ на каждую таблицу успешного файла: `locator = "file:<имя json>;table:<i>"`, `header.form_name` — заголовок
|
Документ на каждую таблицу успешного файла: `locator = "file:<имя json>;table:<i>"`, `header.form_name` — заголовок
|
||||||
таблицы ИС. Подробные правила шагов 2–8 и статусов — `05-domain-rules.md` §11.9, предупреждения документа — там же и
|
таблицы ИС. Если внутри таблицы сменилась форма (сегмент со своей шапкой и кодами другой формы), следующие части —
|
||||||
|
отдельные документы с `locator = "file:<имя json>;table:<i>;form:<k>"`, тип — только по кодам (§11, «Смена формы
|
||||||
|
внутри кандидата»). Подробные правила шагов 2–8 и статусов — `05-domain-rules.md` §11.9, предупреждения документа — там же и
|
||||||
в схеме `statementDocument`.
|
в схеме `statementDocument`.
|
||||||
|
|
||||||
### Статус результата
|
### Статус результата
|
||||||
|
|||||||
@ -34,7 +34,7 @@ multidict==6.7.1
|
|||||||
nats-py==2.14.0
|
nats-py==2.14.0
|
||||||
nkeys==0.2.1
|
nkeys==0.2.1
|
||||||
propcache==0.5.2
|
propcache==0.5.2
|
||||||
pylib_ai_arm_statements==0.1.3
|
pylib_ai_arm_statements==0.1.5
|
||||||
pycparser==3.0
|
pycparser==3.0
|
||||||
pycryptodome==3.23.0
|
pycryptodome==3.23.0
|
||||||
pydantic==2.13.4
|
pydantic==2.13.4
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from arm_statements import (
|
from arm_statements import (
|
||||||
EnvelopeStatus,
|
EnvelopeStatus,
|
||||||
|
FormCandidate,
|
||||||
Rules,
|
Rules,
|
||||||
StatementDocument,
|
StatementDocument,
|
||||||
StatementWarning,
|
StatementWarning,
|
||||||
@ -17,6 +18,7 @@ from arm_statements import (
|
|||||||
envelope_status,
|
envelope_status,
|
||||||
make_warning,
|
make_warning,
|
||||||
normalize_candidate,
|
normalize_candidate,
|
||||||
|
split_by_form,
|
||||||
)
|
)
|
||||||
|
|
||||||
from src.contract import ErrorCode
|
from src.contract import ErrorCode
|
||||||
@ -107,18 +109,22 @@ def _normalize_file(
|
|||||||
header = header_cells(data_ocr)
|
header = header_cells(data_ocr)
|
||||||
for i, table in enumerate(tables):
|
for i, table in enumerate(tables):
|
||||||
try:
|
try:
|
||||||
result = normalize_candidate(table_candidate(name, i, table, header, rules), rules)
|
# ББ и ОФР одной таблицей ИС — части по смене формы (`split_by_form`, AURORA-1734)
|
||||||
|
parts: tuple[FormCandidate, ...] = split_by_form(table_candidate(name, i, table, header, rules), rules)
|
||||||
except Exception as exc: # noqa: BLE001 -- изоляция разбора одной таблицы (P5-9)
|
except Exception as exc: # noqa: BLE001 -- изоляция разбора одной таблицы (P5-9)
|
||||||
text = f"{type(exc).__name__}: {str(exc)[:EXCEPTION_TEXT_LIMIT]}"
|
warnings.append(_table_parse_error(exc, f"file:{name};table:{i}", rules))
|
||||||
warnings.append(
|
|
||||||
make_warning(
|
|
||||||
WarningCode.TABLE_PARSE_ERROR,
|
|
||||||
f"BACKEND_PARSING_ERROR: {text}",
|
|
||||||
rules,
|
|
||||||
locator=f"file:{name};table:{i}",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
if result.document is not None:
|
for part in parts:
|
||||||
documents.append(result.document)
|
try:
|
||||||
warnings.extend(result.file_warnings)
|
result = normalize_candidate(part, rules)
|
||||||
|
except Exception as exc: # noqa: BLE001 -- изоляция разбора одной части таблицы (P5-9)
|
||||||
|
warnings.append(_table_parse_error(exc, part.locator, rules))
|
||||||
|
continue
|
||||||
|
if result.document is not None:
|
||||||
|
documents.append(result.document)
|
||||||
|
warnings.extend(result.file_warnings)
|
||||||
|
|
||||||
|
|
||||||
|
def _table_parse_error(exc: Exception, locator: str, rules: Rules) -> StatementWarning:
|
||||||
|
text = f"{type(exc).__name__}: {str(exc)[:EXCEPTION_TEXT_LIMIT]}"
|
||||||
|
return make_warning(WarningCode.TABLE_PARSE_ERROR, f"BACKEND_PARSING_ERROR: {text}", rules, locator=locator)
|
||||||
|
|||||||
@ -186,3 +186,34 @@ def test_bad_entry_and_parse_error_through_answer_set() -> None:
|
|||||||
assert "Expecting value" in res.error.message
|
assert "Expecting value" in res.error.message
|
||||||
[w] = res.file_warnings
|
[w] = res.file_warnings
|
||||||
assert w.locator == "file:j.json"
|
assert w.locator == "file:j.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_balance_and_income_in_one_table_split_by_form() -> None:
|
||||||
|
"""ББ и ОФР одной таблицей ИС (АК-ЮЛ, AURORA-1734): шапка ОФР внутри таблицы режет её на два документа."""
|
||||||
|
res = normalize_answer_set(merged(("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE + INCOME)))), RULES)
|
||||||
|
|
||||||
|
assert [(d.locator, d.document_type) for d in res.documents] == [
|
||||||
|
("file:j.json;table:0", DocumentType.BALANCE),
|
||||||
|
("file:j.json;table:0;form:1", DocumentType.INCOME),
|
||||||
|
]
|
||||||
|
balance, income = res.documents
|
||||||
|
assert {line.line_code for line in balance.lines} == {row[1] for row in BALANCE[1:]}
|
||||||
|
assert {line.line_code for line in income.lines} == {row[1] for row in INCOME[1:]}
|
||||||
|
assert [c.period_date for c in income.columns] == [date(2024, 12, 31)]
|
||||||
|
assert "TYPE_CONFLICT" not in {w.code for w in income.warnings}
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_exception_keeps_other_part(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Ошибка разбора части `;form:1` — `TABLE_PARSE_ERROR` с её locator, документ первой части остаётся."""
|
||||||
|
real = answer_set_module.normalize_candidate
|
||||||
|
|
||||||
|
def flaky(candidate: Any, rules: Any = None) -> Any:
|
||||||
|
if candidate.locator.endswith(";form:1"):
|
||||||
|
raise RuntimeError("сломалось")
|
||||||
|
return real(candidate, rules)
|
||||||
|
|
||||||
|
monkeypatch.setattr(answer_set_module, "normalize_candidate", flaky)
|
||||||
|
res = normalize_answer_set(merged(("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE + INCOME)))), RULES)
|
||||||
|
|
||||||
|
assert [d.document_type for d in res.documents] == [DocumentType.BALANCE]
|
||||||
|
assert [(w.code, w.locator) for w in res.file_warnings] == [("TABLE_PARSE_ERROR", "file:j.json;table:0;form:1")]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user