Avoid false garbage classification for UPD mentions
Deploy contracts-flask / validate (push) Canceled after 0s
Deploy contracts-flask / validate (push) Canceled after 0s
This commit is contained in:
@@ -81,3 +81,57 @@ Kubernetes проверялся только командами чтения ч
|
|||||||
- frontend upload-тесты — успешно;
|
- frontend upload-тесты — успешно;
|
||||||
- layer 2 upload-тесты — успешно;
|
- layer 2 upload-тесты — успешно;
|
||||||
- архивы `duplicates_inside.zip` и `mixed_with_error.zip` прошли `unzip -t`.
|
- архивы `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`.
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
"""Конфигурация приложения — все настройки в одном месте."""
|
"""Конфигурация приложения — все настройки в одном месте."""
|
||||||
import os
|
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_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
|
||||||
LLM_KEY = os.getenv("LLM_API_KEY", "")
|
LLM_KEY = os.getenv("LLM_API_KEY", "")
|
||||||
|
|||||||
@@ -47,9 +47,13 @@ _GARBAGE_HEADER_MARKERS = [
|
|||||||
'СЧЕТ-ФАКТУРА', 'СЧЕТ НА ОПЛАТУ', 'АКТ СВЕРКИ',
|
'СЧЕТ-ФАКТУРА', 'СЧЕТ НА ОПЛАТУ', 'АКТ СВЕРКИ',
|
||||||
'АКТ ОКАЗАННЫХ УСЛУГ', 'АКТ ВЫПОЛНЕННЫХ РАБОТ',
|
'АКТ ОКАЗАННЫХ УСЛУГ', 'АКТ ВЫПОЛНЕННЫХ РАБОТ',
|
||||||
'ПЛАТЁЖНОЕ ПОРУЧЕНИЕ', 'УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ',
|
'ПЛАТЁЖНОЕ ПОРУЧЕНИЕ', 'УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ',
|
||||||
'УПД', 'ПЛАТЕЖНОЕ ПОРУЧЕНИЕ',
|
'ПЛАТЕЖНОЕ ПОРУЧЕНИЕ',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_GARBAGE_HEADER_RE = re.compile(
|
||||||
|
r'(?m)^\s*(?:УПД|УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ ДОКУМЕНТ)\b'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_garbage_by_filename(filename: str) -> bool:
|
def _is_garbage_by_filename(filename: str) -> bool:
|
||||||
"""Stage 1: regex по имени файла — быстро, 0 токенов."""
|
"""Stage 1: regex по имени файла — быстро, 0 токенов."""
|
||||||
@@ -59,7 +63,7 @@ def _is_garbage_by_filename(filename: str) -> bool:
|
|||||||
def _is_garbage_by_header(text: str) -> bool:
|
def _is_garbage_by_header(text: str) -> bool:
|
||||||
"""Stage 2: ключевые слова в первых 2KB текста — быстро, 0 токенов."""
|
"""Stage 2: ключевые слова в первых 2KB текста — быстро, 0 токенов."""
|
||||||
header = text[:2000].upper()
|
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):
|
def _call_llm_classify(header_text, llm_client=None):
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<img src="/static/logo.svg" alt="Nubes">
|
<img src="/static/logo.svg" alt="Nubes">
|
||||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.19</span></span>
|
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.20</span></span>
|
||||||
<div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);">
|
<div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);">
|
||||||
<span id="stepUpload">○ Загрузка</span><span>→</span>
|
<span id="stepUpload">○ Загрузка</span><span>→</span>
|
||||||
<span id="stepClassify">○ Классификация</span><span>→</span>
|
<span id="stepClassify">○ Классификация</span><span>→</span>
|
||||||
@@ -224,11 +224,11 @@
|
|||||||
import { listZipFiles } from '/upload/zip/list_zip_files.js';
|
import { listZipFiles } from '/upload/zip/list_zip_files.js';
|
||||||
window.listZipFiles = listZipFiles;
|
window.listZipFiles = listZipFiles;
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/state.js?v=2.0.19"></script>
|
<script src="/static/state.js?v=2.0.20"></script>
|
||||||
<script src="/static/app_utils.js?v=2.0.19"></script>
|
<script src="/static/app_utils.js?v=2.0.20"></script>
|
||||||
<script src="/static/files.js?v=2.0.19"></script>
|
<script src="/static/files.js?v=2.0.20"></script>
|
||||||
<script src="/static/groups.js?v=2.0.19"></script>
|
<script src="/static/groups.js?v=2.0.20"></script>
|
||||||
<script src="/static/compare.js?v=2.0.19"></script>
|
<script src="/static/compare.js?v=2.0.20"></script>
|
||||||
<script src="/static/app.js?v=2.0.19"></script>
|
<script src="/static/app.js?v=2.0.20"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ class TestGarbageHeader:
|
|||||||
def test_not_garbage(self):
|
def test_not_garbage(self):
|
||||||
assert _is_garbage_by_header("ДОГОВОР № 03700_1 об оказании услуг") is False
|
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:
|
class TestSafeJsonParse:
|
||||||
def test_valid(self):
|
def test_valid(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user