Files
contracts-flask/History/2026-08-27-classify-stuck-at-68.md
T
“Naeel” e5c7c88073
Deploy contracts-flask / validate (push) Canceled after 0s
Avoid false garbage classification for UPD mentions
2026-08-27 10:44:48 +03:00

7.3 KiB
Raw Blame History

Классификация застряла на 68/84: диагностика 2026-08-27

Наблюдение

На экране классификация остаётся на 68/84, хотя контейнер продолжает работать. Проверка через ВМ:

GET /api/batch-progress?batch=acbdffe5-e3ec-43f4-ab61-84861cac35bd
{"counts":{"classified":68,"garbage":16},"ok":true,"total":84}

Повторный запрос вернул то же значение. Kubernetes показывает pod pythonk8s-574b5cf9b4-z4wxz в состоянии Running, Ready 1/1, RESTARTS 0. Событий Kubernetes нет. На ВМ contracts.service активен, convert_server.py запущен в одном экземпляре.

Причина в коде

site/routes/api_bp.py возвращает раздельные счётчики classified, failed и garbage, а total равен числу всех документов батча.

site/static/app.js в runClassify() считает завершённые документы так:

var done = (d.counts.classified || 0) + (d.counts.failed || 0);

garbage в эту сумму не включён. Для текущего батча:

done = 68 classified + 0 failed = 68
total = 84
garbage = 16
фактически завершено = 68 + 0 + 16 = 84

Поэтому условие done >= total никогда не выполняется, polling не останавливается, а кнопка остаётся на 68/84. Это ошибка фронтендового подсчёта прогресса, а не зависание Kubernetes или LLM на 68-м файле.

Исправление

В site/static/app.js к числу завершённых документов добавлен конечный статус garbage:

var done = (d.counts.classified || 0)
		 + (d.counts.failed || 0)
		 + (d.counts.garbage || 0);

Версия приложения повышена с 2.0.17 до 2.0.18; cache-buster статических скриптов обновлён в site/templates/index.html.

Kubernetes проверялся только командами чтения через ВМ: kubectl get, kubectl describe, kubectl top, kubectl logs.

Дополнительная проверка загрузки ZIP

При проверке загрузки разными способами обнаружен отдельный дефект связи файлов с архивом. expandZipClient() создаёт для каждого распакованного файла поле zip_source, а uploadFile(file, onProgress, zipSource) умеет передавать его в /api/upload_refs. Однако вызов uploadFile() в цикле onFilesSelected() передавал только два аргумента. Поэтому backend получал zip_source=null, и связь с исходным ZIP терялась.

Проверены архивы:

  • testgen/out/zips/duplicates_inside.zip — целый ZIP, два файла с одинаковым именем;
  • testgen/out/zips/mixed_with_error.zip — целый ZIP, корректный и повреждённый DOCX.

Исправление применено в site/static/files.js: третьим аргументом передаётся entry.zip_source. Для обычных файлов значение null, поэтому их поведение не изменяется. Версия приложения повышена с 2.0.18 до 2.0.19, cache-buster обновлён в site/templates/index.html.

Проверки исправления

  • node --check site/static/files.js — успешно;
  • 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-groups200, создан 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.