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

160 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Сессия #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_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
```
---
## Выученные уроки
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)` — повторные запуски создадут несколько строк-итогов (допустимо)