6.1 KiB
Сессия #5 — SSE-парсинг, интерактивный прогресс, отказ от LLM
Дата: 2026-06-16 Версия: v1.0 → v1.5 (5 повышений) Коммиты: fa4c7fe → 435f617 (6 шт.)
Контекст
Пользователь (Наиль) захотел:
- Видеть интерактивный прогресс при обработке файлов (имя + таймер в секундах)
- После обработки — сводка: общее время, байты
- Никакого LLM пока — только парсинг
- Вспомнил что очередь файлов (JS-слой) уже была реализована в коммите
17c4841
Шаг 1: SSE-поток для LLM (v1.1, fa4c7fe)
Что сделано:
_processв app.py переделан с блокирующего POST на SSE-поток (text/event-stream)- Маршрут
/process/<cid>сменён на GET (нужно дляEventSource) - События:
file_start→file_done/file_error→summary - В шаблоне — 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
Выученные уроки
- ⛔ Не фантазировать про инфраструктуру. Managed service ≠ Kubernetes. Деплой =
git push. - Версия — всегда. После каждого изменения кода — bump.
- ZIP требует особой обработки. Недостаточно просто распарсить — нужно создать документы для каждого файла внутри.
- Имена должны совпадать между клиентом (JSZip) и сервером (zipfile) — иначе
findRowByNameне находит строку. replace_string_in_fileпортит файлы — для больших правок использовать heredoc (cat > file).