diff --git a/History/2026-08-27-classify-stuck-at-68.md b/History/2026-08-27-classify-stuck-at-68.md index b8f99e2..378a16c 100644 --- a/History/2026-08-27-classify-stuck-at-68.md +++ b/History/2026-08-27-classify-stuck-at-68.md @@ -81,3 +81,57 @@ Kubernetes проверялся только командами чтения ч - frontend upload-тесты — успешно; - layer 2 upload-тесты — успешно; - архивы `duplicates_inside.zip` и `mixed_with_error.zip` прошли `unzip -t`. + +## Реальный end-to-end тест сверки + +27.08.2026 выполнен тест через deployed API `v2.0.19` на batch +`0dd0d3a7-3e52-4888-95eb-1b6d3e37f40e`. + +Загружены через VM-поток два реальных DOCX: + +- `договор-XXX001-03700.docx` — распарсен, 20 элементов; +- `допник-1-XXX001-03700.docx` — распарсен, 16 элементов. + +Классификация завершилась `2/2`, но результат классификации неожиданен: + +- допсоглашение попало в группу `03700`; +- базовый договор попал в `__unresolved__` со статусом `garbage` и причиной + отсутствия matching-договора. + +Это функциональный дефект/проблема классификации тестовой пары: без базового +договора группа не проверяет корректное сравнение договора с приложением. + +Тем не менее реальный pipeline сверки для сформированной группы проверен: + +- `/api/apply-groups` — `200`, создан contract ID + `b859e813-bac9-4eb9-82b5-6b5666f2ba4a`; +- `/process-v2` — SSE завершён событием `complete`; +- LLM вернул `6` операций в режиме `partial`; +- применено: `added=6`, `updated=0`, `deleted=0`; +- ошибок соединения и SSE не было; +- время pipeline: `9.4с`. + +Следующий обязательный тест для проверки matching — загрузить базовый договор с +точным именем/содержимым, которое классификатор ожидает как базовый, и повторить +ту же пару. Код после этого прогона не изменялся. + +## Исправление ложного garbage для базового договора + +При повторной проверке реального файла `договор-XXX001-03700.docx` установлена +точная причина ошибочной классификации: второй garbage-фильтр искал подстроку +`УПД` в первых 2000 символах. В договоре эта аббревиатура встречается в +обычном условии оплаты: «на основании УПД и счета». Из-за этого договор +получал `garbage=header_keywords`, хотя его заголовок начинается со слова +«Договор». + +Исправление в `site/services/classify.py`: + +- удалено общее substring-срабатывание для `УПД`; +- добавлено распознавание `УПД` и полного названия только с начала строки, + когда это заголовок самого документа. + +Добавлены регрессионные тесты: упоминание УПД внутри договора не является +garbage, а заголовок документа `УПД № ...` является garbage. + +Версия приложения повышена до `2.0.20`, cache-buster обновлён в +`site/templates/index.html`. diff --git a/site/config.py b/site/config.py index 90907e5..efca7bc 100644 --- a/site/config.py +++ b/site/config.py @@ -1,7 +1,7 @@ """Конфигурация приложения — все настройки в одном месте.""" import os -VERSION = "2.0.19" +VERSION = "2.0.20" LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions") LLM_KEY = os.getenv("LLM_API_KEY", "") diff --git a/site/services/classify.py b/site/services/classify.py index 3498075..8353ac2 100644 --- a/site/services/classify.py +++ b/site/services/classify.py @@ -47,9 +47,13 @@ _GARBAGE_HEADER_MARKERS = [ 'СЧЕТ-ФАКТУРА', 'СЧЕТ НА ОПЛАТУ', 'АКТ СВЕРКИ', 'АКТ ОКАЗАННЫХ УСЛУГ', 'АКТ ВЫПОЛНЕННЫХ РАБОТ', 'ПЛАТЁЖНОЕ ПОРУЧЕНИЕ', 'УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ', - 'УПД', 'ПЛАТЕЖНОЕ ПОРУЧЕНИЕ', + 'ПЛАТЕЖНОЕ ПОРУЧЕНИЕ', ] +_GARBAGE_HEADER_RE = re.compile( + r'(?m)^\s*(?:УПД|УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ ДОКУМЕНТ)\b' +) + def _is_garbage_by_filename(filename: str) -> bool: """Stage 1: regex по имени файла — быстро, 0 токенов.""" @@ -59,7 +63,7 @@ def _is_garbage_by_filename(filename: str) -> bool: def _is_garbage_by_header(text: str) -> bool: """Stage 2: ключевые слова в первых 2KB текста — быстро, 0 токенов.""" header = text[:2000].upper() - return any(marker in header for marker in _GARBAGE_HEADER_MARKERS) + return any(marker in header for marker in _GARBAGE_HEADER_MARKERS) or bool(_GARBAGE_HEADER_RE.search(header)) def _call_llm_classify(header_text, llm_client=None): diff --git a/site/templates/index.html b/site/templates/index.html index 6ee6aee..843f736 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -73,7 +73,7 @@
Nubes - Сверка договоров — LLM AI-driven Event Sourcing v2.0.19 + Сверка договоров — LLM AI-driven Event Sourcing v2.0.20
○ Загрузка ○ Классификация @@ -224,11 +224,11 @@ import { listZipFiles } from '/upload/zip/list_zip_files.js'; window.listZipFiles = listZipFiles; - - - - - - + + + + + + diff --git a/tests/test_classify.py b/tests/test_classify.py index 83de0d9..5f74f29 100644 --- a/tests/test_classify.py +++ b/tests/test_classify.py @@ -36,6 +36,13 @@ class TestGarbageHeader: def test_not_garbage(self): assert _is_garbage_by_header("ДОГОВОР № 03700_1 об оказании услуг") is False + def test_upd_mention_in_contract_is_not_garbage(self): + text = "Договор № 03700. Оплата производится на основании УПД и счета." + assert _is_garbage_by_header(text) is False + + def test_upd_document_header_is_garbage(self): + assert _is_garbage_by_header("УПД № 123 от 01.01.2026") is True + class TestSafeJsonParse: def test_valid(self):