v1.0.177: History — объединение history+History, 5 подпапок
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# Архитектура Contracts App
|
||||
|
||||
## Обзор
|
||||
|
||||
Сервис **Сверка договоров** — автоматизированная обработка договоров и допников (docx/pdf)
|
||||
с извлечением структурированных данных, отслеживанием изменений и восстановлением
|
||||
истории договора во времени.
|
||||
|
||||
## Принципы
|
||||
|
||||
1. **НЕ МОНОЛИТ** — каждый слой независим, отдельный файл, своя зона ответственности
|
||||
2. **Данные не покидают облако** — всё в PostgreSQL внутри кластера
|
||||
3. **Ничего не терять** — парсер отдаёт полный слепок документа, LLM решает что важно
|
||||
4. **Исключения — не фантазировать** — нерешаемые подзадачи отмечать явно
|
||||
|
||||
---
|
||||
|
||||
## Слои приложения
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ app.py │
|
||||
│ ContractsApp (сборка) │
|
||||
├──────────┬──────────┬──────────┬────────────┤
|
||||
│ db.py │ parser.py│ test_ │ (будущие) │
|
||||
│ (БД) │ (парсинг)│ routes.py │ llm.py │
|
||||
│ │ │ (/test) │ upload.py│
|
||||
└──────────┴──────────┴──────────┴────────────┘
|
||||
```
|
||||
|
||||
| Слой | Файл | Что делает | Статус |
|
||||
|---|---|---|---|
|
||||
| Ядро | `app.py` | Flask-приложение, инициализация, регистрация Blueprint | ✅ |
|
||||
| БД | `db.py` | `connect()`, `query()`, `_pg_connect()` | ✅ |
|
||||
| Тесты | `test_routes.py` | Blueprint `/test` — мост к БД извне | ✅ |
|
||||
| Парсер | `parser.py` | `parse(bytes, mime) → elements` для docx/pdf/doc/zip | ✅ |
|
||||
| LLM | `llm.py` | Нормализация строк через aillm.ru (120B) | ⬜ |
|
||||
| Загрузка | `upload.py` | Приём файлов, сохранение в БД | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## Поток обработки документа
|
||||
|
||||
```
|
||||
Пользователь
|
||||
│
|
||||
▼
|
||||
POST /upload (файл .docx/.pdf/.doc/.zip)
|
||||
│
|
||||
▼
|
||||
upload.py: сохранить в contract_docs (original_bytes)
|
||||
│
|
||||
▼
|
||||
parser.py: parse(bytes, mime) → elements JSON
|
||||
│
|
||||
▼
|
||||
db.py: сохранить parsed_json в contract_docs
|
||||
│
|
||||
▼
|
||||
llm.py: отправить elements → LLM → нормализованные spec_rows
|
||||
│
|
||||
▼
|
||||
db.py: сохранить в spec_rows, сравнить с предыдущими → spec_history
|
||||
│
|
||||
▼
|
||||
GET /contract/{id}/history → полная история изменений
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Схема БД (план)
|
||||
|
||||
```
|
||||
contract_docs — исходные файлы + сырой парсинг
|
||||
id UUID PK
|
||||
contract_id → contracts.id
|
||||
filename TEXT
|
||||
mime_type TEXT
|
||||
original_bytes BYTEA ← сам файл
|
||||
parsed_json JSONB ← выдача parser.py
|
||||
created_at TIMESTAMPTZ
|
||||
|
||||
contracts — договоры
|
||||
id UUID PK
|
||||
number TEXT ← номер договора
|
||||
client TEXT ← клиент
|
||||
date DATE
|
||||
status TEXT
|
||||
|
||||
supplements — допники
|
||||
id UUID PK
|
||||
contract_id → contracts.id
|
||||
number TEXT
|
||||
date DATE
|
||||
type TEXT ← новый / изменение / расторжение
|
||||
doc_id → contract_docs.id
|
||||
|
||||
spec_rows — строки спецификаций
|
||||
id UUID PK
|
||||
supplement_id → supplements.id
|
||||
row_num INT
|
||||
name TEXT ← наименование услуги
|
||||
price NUMERIC
|
||||
qty NUMERIC
|
||||
sum NUMERIC
|
||||
date_start DATE
|
||||
date_end DATE
|
||||
|
||||
spec_history — история изменений
|
||||
id UUID PK
|
||||
spec_row_id → spec_rows.id
|
||||
supplement_id → supplements.id
|
||||
change_type TEXT ← added / changed / deleted / unchanged
|
||||
old_values JSONB
|
||||
new_values JSONB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API эндпоинты
|
||||
|
||||
| Метод | Путь | Слой | Что |
|
||||
|---|---|---|---|
|
||||
| GET | `/` | app.py | Главная (HTML) |
|
||||
| GET | `/health` | app.py | Health check → "OK" |
|
||||
| GET | `/test` | test_routes | Статус БД + список команд |
|
||||
| POST | `/test` `{"action":"status"}` | test_routes | Статус БД |
|
||||
| POST | `/test` `{"action":"createdb"}` | test_routes | Создать БД contracts |
|
||||
| POST | `/test` `{"action":"tables"}` | test_routes | Список таблиц |
|
||||
| POST | `/test` `{"action":"sql","sql":"..."}` | test_routes | Произвольный SQL |
|
||||
|
||||
---
|
||||
|
||||
## Технологии
|
||||
|
||||
| Компонент | Выбор |
|
||||
|---|---|
|
||||
| Язык | Python 3.12 |
|
||||
| Фреймворк | Flask |
|
||||
| БД | PostgreSQL (внутрикластерный) |
|
||||
| Парсинг docx | python-docx |
|
||||
| Парсинг .doc | LibreOffice (headless) |
|
||||
| Парсинг PDF | pdfplumber |
|
||||
| LLM | aillm.ru API (120B модель) |
|
||||
| Деплой | pythonk8s.services.ngcloud.ru |
|
||||
| Репозиторий | gitea.services.ngcloud.ru/Nail/contracts-app.git |
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация (переменные окружения)
|
||||
|
||||
| Переменная | Назначение |
|
||||
|---|---|
|
||||
| `DB_HOST` | Хост PostgreSQL |
|
||||
| `DB_PORT` | Порт (5432) |
|
||||
| `DB_NAME` | Имя БД (contracts) |
|
||||
| `DB_USER` | Пользователь |
|
||||
| `DB_PASS` | Пароль |
|
||||
| `DB_SSLMODE` | SSL mode (disable) |
|
||||
| `LLM_API_KEY` | Ключ aillm.ru (будет) |
|
||||
| `LLM_API_URL` | URL LLM API (будет) |
|
||||
@@ -0,0 +1,69 @@
|
||||
# Блок-схема сервиса Contracts
|
||||
|
||||
## Полный пайплайн обработки допника
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["📄 POST /api/contracts/{id}/supplements\n(file.docx)"] --> B["💾 documents.original_bytes\n(status=uploaded)"]
|
||||
B --> C["🔧 parser.parse()\nbytes → elements JSON"]
|
||||
C --> D["📝 textify.to_text()\nelements → линейный текст"]
|
||||
D --> E["📄 documents.parsed_text\n(status=parsed)"]
|
||||
E --> F["🤖 extractor.extract()\nтекст → LLM → строки JSON"]
|
||||
F --> G["📊 spec_rows\n(row_num, name, price, qty, sum, date)"]
|
||||
G --> H["🔄 differ.diff()\nсравнение с пред. допником"]
|
||||
H --> I["📋 spec_history\n(added/changed/deleted/unchanged)"]
|
||||
I --> J["✅ ответ API\n{rows, diff_summary}"]
|
||||
```
|
||||
|
||||
## Архитектура слоёв
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph "Внешний мир"
|
||||
USER["👤 Пользователь"]
|
||||
end
|
||||
|
||||
subgraph "Flask"
|
||||
APP["app.py\nсборка"]
|
||||
API["api.py\n/contracts"]
|
||||
UPL["upload.py\n/upload"]
|
||||
TEST["test_routes.py\n/test"]
|
||||
end
|
||||
|
||||
subgraph "Бизнес-логика"
|
||||
PARSER["parser.py\nbytes→JSON"]
|
||||
TEXTIFY["textify.py\nJSON→текст"]
|
||||
LLM["llm_client.py\nHTTP/2→LLM"]
|
||||
EXTRACT["extractor.py\nтекст→строки"]
|
||||
DIFF["differ.py\nсравнение"]
|
||||
end
|
||||
|
||||
subgraph "Данные"
|
||||
DB["db.py\nconnect/query"]
|
||||
SCH["schema.py\nDDL"]
|
||||
PG[("PostgreSQL\n5 таблиц")]
|
||||
end
|
||||
|
||||
USER -->|"POST docx"| API
|
||||
USER -->|"GET /test"| TEST
|
||||
API --> PARSER
|
||||
PARSER --> TEXTIFY
|
||||
TEXTIFY --> EXTRACT
|
||||
EXTRACT --> LLM
|
||||
EXTRACT --> DIFF
|
||||
API --> DB
|
||||
DB --> PG
|
||||
APP --> SCH
|
||||
SCH --> DB
|
||||
```
|
||||
|
||||
## Цепочка данных
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["docx\n(байты)"] -->|parser| B["elements\nJSON"]
|
||||
B -->|textify| C["текст\nстрока"]
|
||||
C -->|extractor + LLM| D["spec_rows\nJSON"]
|
||||
D -->|differ| E["changes\nJSON"]
|
||||
E -->|api| F["ответ\nпользователю"]
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
# Пайплайн обработки договора
|
||||
|
||||
## Схема
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 👤 ПОЛЬЗОВАТЕЛЬ │
|
||||
│ Открывает / → выбирает файлы → жмёт «Обработать» │
|
||||
└──────────────────────────┬──────────────────────────────────────────┘
|
||||
│ multipart/form-data (files)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ app.py: _upload_page() POST │
|
||||
│ Принимает файлы, запускает пайплайн │
|
||||
└──────────────────────────┬──────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
file.docx file.pdf file.zip
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ① parser.py │
|
||||
│ docx → python-docx → elements JSON │
|
||||
│ pdf → pdfplumber → elements JSON │
|
||||
│ doc → libreoffice → docx → python-docx │
|
||||
│ zip → zipfile → каждый файл рекурсивно │
|
||||
│ │
|
||||
│ Выход: [{type:"paragraph", style, text}, {type:"table", rows}] │
|
||||
│ НИЧЕГО не фильтрует, не теряет │
|
||||
└──────────────────────────┬──────────────────────────────────────────┘
|
||||
│ elements JSON
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ② textify.py │
|
||||
│ elements → линейный текст │
|
||||
│ │
|
||||
│ [No Spacing] Приложение № 1 │
|
||||
│ [Heading 3] Состав и стоимость Услуг │
|
||||
│ --- Таблица (9×6) --- │
|
||||
│ | № | Наименование | Цена | Объем | Сумма | Дата | │
|
||||
│ | 1 | Аренда стойко-места | 213 905 | 3 | 641 716 | 01.04.2026 | │
|
||||
└──────────────────────────┬──────────────────────────────────────────┘
|
||||
│ текст (строка)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ③ extractor.py + llm_client.py │
|
||||
│ текст → промпт → LLM (gpt-oss-120b) → JSON │
|
||||
│ │
|
||||
│ Промпт: «Найди таблицы услуг, извлеки строки в JSON» │
|
||||
│ Ответ: [{"row_num":1, "name":"Аренда...", "price":213905, │
|
||||
│ "qty":3, "sum":641716, "date_start":"2026-04-01"}, ...] │
|
||||
└──────────────────────────┬──────────────────────────────────────────┘
|
||||
│ строки спецификации
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ④ differ.py │
|
||||
│ Сравнение с предыдущим допником │
|
||||
│ │
|
||||
│ rows_old vs rows_new → changes: │
|
||||
│ added: новая услуга │
|
||||
│ changed: цена 213905 → 250000, дата 01.04 → 25.04 │
|
||||
│ deleted: услуга убрана │
|
||||
│ unchanged: без изменений │
|
||||
└──────────────────────────┬──────────────────────────────────────────┘
|
||||
│ изменения
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ⑤ Сохранение в PostgreSQL │
|
||||
│ │
|
||||
│ documents ← исходный файл + распарсенный текст │
|
||||
│ contracts ← договор │
|
||||
│ supplements ← допник │
|
||||
│ spec_rows ← строки спецификации │
|
||||
│ spec_history ← история изменений │
|
||||
└──────────────────────────┬──────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Ответ пользователю: HTML-страница с таблицей результатов │
|
||||
│ «Извлечено 14 строк, добавлено 14, изменено 0, удалено 0» │
|
||||
│ + полная таблица услуг с ценами │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Кратко
|
||||
|
||||
```
|
||||
файлы → parser → textify → LLM → differ → PostgreSQL → HTML-страница
|
||||
```
|
||||
|
||||
## Слои
|
||||
|
||||
| # | Слой | Что делает |
|
||||
|---|---|---|
|
||||
| ① | `parser.py` | docx/pdf/doc/zip → JSON |
|
||||
| ② | `textify.py` | JSON → текст |
|
||||
| ③ | `extractor.py` + `llm_client.py` | текст → LLM → строки |
|
||||
| ④ | `differ.py` | сравнение с предыдущим |
|
||||
| ⑤ | `db.py` + `schema.py` | сохранение в БД |
|
||||
| — | `app.py` | сборка, приём формы, отдача HTML |
|
||||
@@ -0,0 +1,71 @@
|
||||
# Сверка договоров — описание сервиса
|
||||
|
||||
## Что делает
|
||||
|
||||
Сервис автоматически сравнивает договоры и дополнительные документы облачного провайдера (colocation, ЦОД). Юрист загружает файлы (.docx/.doc/.pdf), система извлекает спецификации услуг и при помощи LLM находит изменения: что добавилось, изменилось, удалилось.
|
||||
|
||||
## Как устроен пайплайн
|
||||
|
||||
```
|
||||
Загрузка → Парсинг → Порядок → LLM-сравнение → Результаты
|
||||
```
|
||||
|
||||
1. **Загрузка** — файлы принимаются через веб-интерфейс. Поддерживаются .docx, .doc (старый Word), .pdf, а также .zip с несколькими файлами.
|
||||
|
||||
2. **Парсинг** — каждый файл автоматически разбирается: извлекаются таблицы и текст. Используется Apache POI (Java) для Word-документов и PDFBox для PDF. Результат — структурированный JSON (elements_json) и текстовое представление.
|
||||
|
||||
3. **Порядок** — файлы можно переставить стрелками ↕. Первый в списке считается базовым договором, остальные — дополнительные документы к нему.
|
||||
|
||||
4. **LLM-сравнение** — каждый дополнительный документ последовательно сравнивается с текущей спецификацией. Модель (gpt-oss-120b) получает промпт с текущим списком услуг, текстом дополнительного документа и возвращает операции:
|
||||
- **ADD** — новая услуга
|
||||
- **UPDATE** — изменение цены, количества, названия
|
||||
- **DELETE** — услуга исключена
|
||||
- **UNRESOLVED** — не удалось однозначно сопоставить
|
||||
|
||||
5. **Результаты** — накапливаются по цепочке дополнительных документов (Event Sourcing). Каждый следующий дополнительный документ учитывает изменения из предыдущих. Итоговая спецификация — сумма всех применённых операций.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
Браузер (index.cfm + JS)
|
||||
│
|
||||
├── загрузка файлов ──→ VM (Python, convert_server.py)
|
||||
│ │
|
||||
│ ├── /convert-doc ──→ Lucee (parser.cfm)
|
||||
│ ├── /process-v2 ───→ Lucee (apply_events.cfm)
|
||||
│ └── LLM (api.aillm.ru, gpt-oss-120b)
|
||||
│
|
||||
└── API ──→ Lucee (CFML на k8s)
|
||||
│
|
||||
└── PostgreSQL 15 (документы, спецификации, события, промпты)
|
||||
```
|
||||
|
||||
| Компонент | Где | Технология |
|
||||
|-----------|-----|------------|
|
||||
| Веб-интерфейс | Lucee 6.0 (k8s) | CFML + JavaScript |
|
||||
| База данных | Внутренний PostgreSQL 15 | JSONB, UUID, advisory locks |
|
||||
| Парсинг документов | Lucee | Apache POI (HWPF/XWPF), PDFBox |
|
||||
| LLM-анализ | Внешняя VM (5.172.178.213) | Python 3, httpx, SSE-стриминг |
|
||||
| Модель | api.aillm.ru | gpt-oss-120b (бесплатно, 8000 токенов) |
|
||||
|
||||
## Event Sourcing
|
||||
|
||||
Изменения не перезаписывают спецификацию — каждая операция сохраняется как событие в `spec_events`. Текущее состояние (`spec_current`) — материализованное представление всех событий.
|
||||
|
||||
Это даёт:
|
||||
- **Аудит** — кто/when/откуда каждая строка
|
||||
- **Откат** — можно пересобрать состояние на любой момент
|
||||
- **Provenance** — ссылка на документ-источник, версию промпта, полный ответ LLM
|
||||
|
||||
## Промпты
|
||||
|
||||
Промпты для LLM хранятся в БД и версионируются. Есть два: для первого документа (извлечение) и для сравнения дополнительных документов. Встроенный редактор с историей версий позволяет улучшать промпты без правки кода: сохранил новую версию → она сразу используется LLM. Старые версии остаются в истории, можно откатиться.
|
||||
|
||||
## Стек
|
||||
|
||||
- **Backend**: Lucee 6.0 (CFML) на Kubernetes
|
||||
- **База**: PostgreSQL 15 (JSONB, UUID, window functions)
|
||||
- **Парсинг**: Apache POI (Java, встроен в Lucee)
|
||||
- **LLM-прокси**: Python 3 + httpx + threading (SSE)
|
||||
- **Модель**: gpt-oss-120b (OpenAI-совместимый API)
|
||||
- **Фронтенд**: ванильный JS + Lucide иконки
|
||||
@@ -0,0 +1,34 @@
|
||||
# Две панели просмотра — архитектура
|
||||
|
||||
**v1.0.86+**
|
||||
|
||||
## Назначение
|
||||
|
||||
Кнопка «Текст» в таблице файлов открывает модальное окно с двумя панелями:
|
||||
|
||||
| Панель | Заголовок | Содержание | Цель |
|
||||
|--------|-----------|------------|------|
|
||||
| Левая | Сырой текст | Плоский textify из elements_json | Проверить что ВЕСЬ текст извлечён |
|
||||
| Правая | Распарсено | Статистика + структурированный textify | Проверить что таблицы/параграфы распознаны верно |
|
||||
|
||||
## Источник данных
|
||||
|
||||
Обе панели из одного `elements_json` (результат парсинга parser.cfm).
|
||||
|
||||
- **Левая** — все параграфы подряд + таблицы в текстовом виде. Сплошной поток.
|
||||
- **Правая** — та же информация + статистика (кол-во элементов, параграфов, таблиц, строк, время парсинга, ошибки).
|
||||
|
||||
## Зачем две панели
|
||||
|
||||
Если парсер ошибся:
|
||||
- Слева текст есть, справа таблица не распозналась → баг парсинга таблиц
|
||||
- Слева текст обрезан, справа элементов мало → баг извлечения текста
|
||||
- Обе панели корректны → парсинг OK
|
||||
|
||||
## Не показывать юзеру
|
||||
|
||||
- `doc_id` — внутренний идентификатор
|
||||
- `supp_id` — внутренний идентификатор
|
||||
- `Тип ДС` — служебная инфа
|
||||
|
||||
Эти поля убрать из правой панели (TODO).
|
||||
Reference in New Issue
Block a user