# Архитектура contracts-flask (актуально на v2.0.16) Сервис сверки договоров. Flask + vanilla JS (без frontend-фреймворков). Managed на Nubes (Штурвал), redeploy через панель (не kubectl). ## Компоненты ``` Браузер (vanilla JS: files.js, compare.js, app.js) │ ├─ PUT файла → ВМ-буфер (WebDAV /contracts-upload/) │ Браузер кладёт файл на ВМ, т.к. managed-gateway рвёт тело >64 КБ. │ ВМ: 5.172.178.213, домен contracts.kube5s.ru. │ ├─ /, /static/*, /templates/* → Managed Flask │ Отдаёт HTML + JS. │ └─ API (SQLite, WAL) → LLM (api.aillm.ru, gpt-oss-120b) /api/upload_refs — бэк сам тянет файл с ВМ (egress не лимитирован) /process-v2 (SSE) — конвейер сравнения /api/classify-* — классификация /chat — чат по спецификации ``` ## БД - **SQLite**, WAL-режим, `/tmp/contracts.db`. - Thread-local соединения (`db/connection.py`), API совместим с PostgreSQL через `_pg_to_sqlite()`. - Таблицы: `documents`, `supplements`, `spec_current`, `spec_events`, `prompts`, `upload_chunks`. ## Event sourcing (спецификация) - `spec_events` — append-only журнал операций (история). - `spec_current` — материализованное текущее состояние (для рендера/сравнения). - `_hash(name, date_start)` = sha256(name.strip().lower() + "|" + normalize_date)[:16] — ключ строки. - `apply_ops()` обрабатывает ADD / UPDATE / DELETE / UNRESOLVED. ## Загрузка файлов 1. `uploadFile()` (files.js) — браузер PUT файла в ВМ-буфер (WebDAV). 2. `POST /api/upload_refs {files:[{name,size,url}]}` — бэк тянет файл с ВМ по `url` (egress без лимита), конвертирует `.doc` → `.docx` (`contracts_upload_sink`), парсит и кладёт в БД. 3. Парсинг: pdfplumber / python-docx → `elements_json`. ## ZIP (важно: клиентское раскрытие) ⚠️ ZIP раскрывается **на клиенте**, НЕ серверным `/unzip-upload` (тот — legacy). - `expandZipClient(f)` (files.js) — рекурсивно раскрывает архив через `window.listZipFiles` (endpoint drhider, подтянут в браузере). - Каждый вложенный файл получает `zip_source = имя архива` для группировки в таблице. - Fallback: если раскрыть не удалось — архив кладётся как есть. - Фильтр вложений: `.pdf`, `.doc`, `.docx`. ## Сравнение (`/process-v2`, SSE) 1. `run_pipeline()` шлёт SSE-события браузеру. 2. Текущая спецификация → текст → LLM → `{mode, ops[]}`. 3. **Трансляция `target_id` → `target_hash`**: LLM возвращает `target_id: "r1"` (индекс строки), а `apply_ops()` читает `target_hash`. Без трансляции UPDATE/DELETE уходили бы в UNRESOLVED. 4. `apply_ops()` пишет `spec_events` + обновляет `spec_current`. ### Режимы LLM - `partial` — точечные ADD/UPDATE/DELETE (UPDATE/DELETE по `target_hash`). - `full_replace` — LLM возвращает полную новую редакцию (все ADD). ⚠️ Перед применением очищается **только** `spec_current` (`clear_current()`), чтобы ADD-строки новой редакции не дублировали старые. История `spec_events` сохраняется. ## Классификация - `api.aillm.ru`, модель `gpt-oss-120b` (конфиденциальные данные — только своя модель). - `ThreadPoolExecutor(max_workers=4)`. - Поля: `doc_type`, `own_number`, `parent_number`, `counterparty`, `doc_date`. ## Конфигурация `site/config.py`: VERSION, LLM_URL/KEY/MODEL, CONVERT_SERVICE_URL, VM_UPLOAD_URL/VM_UPLOAD_PREFIX/VM_UPLOAD_MAX_BYTES (50 МБ), PULL_RETRIES (3). ## Модуль `upload/` Переиспользован из drhider (слои 1–2). Плаг-ин через `sink`: `create_upload_refs_blueprint(cfg, sink=contracts_upload_sink)`. Слои 3–4 (in-memory session) — НЕ используются в contracts.