Files
contracts/History/sessions/session-05-sse-parsing.md

9.2 KiB
Raw Permalink Blame History

Сессия #5 — SSE-парсинг, интерактивный прогресс, отказ от LLM

Дата: 2026-06-16 Версия: v1.0 → v1.8 (8 повышений) Коммиты: fa4c7fe → e0c58fc (8 шт.)


Контекст

Пользователь (Наиль) захотел:

  1. Видеть интерактивный прогресс при обработке файлов (имя + таймер в секундах)
  2. После обработки — сводка: общее время, байты
  3. Никакого LLM пока — только парсинг
  4. Вспомнил что очередь файлов (JS-слой) уже была реализована в коммите 17c4841

Шаг 1: SSE-поток для LLM (v1.1, fa4c7fe)

Что сделано:

  • _process в app.py переделан с блокирующего POST на SSE-поток (text/event-stream)
  • Маршрут /process/<cid> сменён на GET (нужно для EventSource)
  • События: file_startfile_done/file_errorsummary
  • В шаблоне — JavaScript с EventSource, живые таймеры (каждые 500мс)

Ошибка: Назвал деплой «поды в Kubernetes» — пользователь поправил: это managed service на pythonk8s.services.ngcloud.ru, деплой = git push master.

Ошибка: Забыл повысить версию → v1.1 отдельным коммитом.


Шаг 2: Полный отказ от LLM (v1.2, 3cffca7)

Причина: Пользователь сказал «НЕ НАДО ЛЛМ и на экране тоже».

Что сделано:

  • _process_parse — только парсинг, без extractor.extract() и differ.diff()
  • _upload_files — только сохраняет файлы в БД, без парсинга на этом этапе
  • POST / возвращает JSON {"contract_id": "..."} вместо редиректа
  • Весь LLM (extractor, differ, llm_client) убран из основного потока

Интерфейс:

  • Файлы → сразу в таблицу (имя, дата изменения, размер)
  • ZIP — JSZip показывает содержимое в браузере
  • Кнопка «Парсинг» (не «Обработать LLM»)
  • Клиент: POST файлов через fetch, затем SSE /parse/<cid>
  • Прогресс в столбце «Статус»: Xс → ✓ X.Xс
  • Итоговая строка: ✓ Готово: N файлов | размер | время

Шаг 3: ZIP — файлы внутри не парсились (v1.3, 21f8c0b)

Проблема: ZIP-файл парсился как единое целое (✓ 0.2с), а файлы внутри него (допник-1.doc, спецификация.docx...) оставались с в статусе.

Причина: В _parse парсер просто сливал все elements из ZIP в один документ. Файлы из JSZip-превью были только в браузере, сервер про них не знал.

Решение:

  • Сервер сам открывает ZIP (zipfile.ZipFile)
  • Для каждого файла внутри: создаёт отдельный documents + supplements, парсит, шлёт SSE-события
  • JSZip и сервер используют одинаковые имена → findRowByName находит строку
  • ZIP-строка показывает ↗ N файлов вместо галочки

Шаг 4: Подпапки в ZIP (v1.4, d5858d7)

Проблема: Если в ZIP есть подпапки, и в разных папках файлы с одинаковыми именами — конфликт.

Решение: Использовать полный относительный путь (подпапка/файл.docx) вместо basename.

Изменено в двух местах:

  • Клиент: relativePath вместо relativePath.split('/').pop()
  • Сервер: zname вместо zbasename

Шаг 5: Только PDF и Word (v1.5, 435f617)

Проблема: Из ZIP парсились ВСЕ файлы (включая .txt, .xlsx), давая пустые результаты.

Решение: Фильтр по MIME-типу:

  • Сервер: if zmime not in (pdf, docx, doc): continue
  • Клиент (JSZip): if ['docx','doc','pdf'].indexOf(ext) === -1: return

Архитектура (текущая)

app.py:
  POST /            → _upload_files()  → сохранить файлы в БД, вернуть JSON {contract_id}
  GET  /            → _index()         → render_template("upload.html")
  GET  /parse/<cid> → _parse()         → SSE-поток парсинга

_parse():
  1. Для каждого документа договора:
     - Обычный файл → parser_mod.parse() → сохранить parsed_text
     - ZIP → zipfile.ZipFile() → для каждого PDF/Word внутри:
         создать document + supplement → parser_mod.parse() → сохранить
  2. SSE-события: file_start → file_done/file_error → zip_expanded → summary

Выученные уроки

  1. Не фантазировать про инфраструктуру. Managed service ≠ Kubernetes. Деплой = git push.
  2. Версия — всегда. После каждого изменения кода — bump.
  3. ZIP требует особой обработки. Недостаточно просто распарсить — нужно создать документы для каждого файла внутри.
  4. Имена должны совпадать между клиентом (JSZip) и сервером (zipfile) — иначе findRowByName не находит строку.
  5. replace_string_in_file портит файлы — для больших правок использовать heredoc (cat > file).

Шаг 6: Таймаут при загрузке (v1.6, 64b2295)

Проблема: Все файлы отправлялись одним POST-запросом → >30с → Ingress убивал соединение → «NetworkError when attempting to fetch resource». Никакого прогресса загрузки не было видно.

Решение:

  • _upload_files поддерживает ?cid=X — первый файл создаёт договор, остальные добавляются
  • Клиент: загрузка по одному файлу через XHR с upload.onprogress
  • Прогресс: ↑ 0% → ↑ 45% → ↑ 100% → ✓ загружен
  • XHR таймаут: 60 секунд на файл
  • Размеры файлов из ZIP — через zipEntry._data.uncompressedSize
  • Размеры выровнены вправо (.num-cell)

Шаг 7: Прогресс для файлов из ZIP не показывался (v1.7, c710a2d)

Проблема: findRowByName имел условие && !fileQueue[i].isZipChild, поэтому SSE-события для файлов из ZIP не находили строку в таблице — статус оставался пустым.

Причина: в v1.6 findRowByName использовалась и для upload (realFiles) и для parse (все файлы). Фильтр защищал от нахождения zip-child при upload, но ломал parse.

Решение: убрать !fileQueue[i].isZipChild. Для upload это безопасно — realFiles и так фильтрует !isZipChild перед вызовом.

БД: проверена — все 5 файлов из ZIP созданы, 4 распарсены, 1 .doc с пустым parsed_text (нет LibreOffice).


Шаг 8: Баги после полной проверки (v1.8, e0c58fc)

Баг 1 — zip_expanded счётчик: zip_names.append(zname) стоял ДО фильтра PDF/Word, поэтому «↗ N файлов» включал .txt, .xlsx и т.д. Исправлено: перенос после фильтра.

Баг 2 — повторный _parse пересоздаст ZIP-файлы: проверка status='parsed' пропускала только распарсенные, но ZIP имеют статус expanded. При повторном вызове _parse ZIP обрабатывался заново, создавая дубликаты. Исправлено: status IN ('parsed','expanded').

Другие проверки (без ошибок):

  • resetBtn() — function declaration, hoisting работает, вызов до определения корректен
  • removeFile — удаление детей перед родителем, индексы не смещаются (дети всегда после родителя)
  • fileTable.appendChild(summaryRow) — повторные запуски создадут несколько строк-итогов (допустимо)