init: README, примеры договоров, история и архитектура проекта
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
FILES
|
||||||
|
contracts-app/
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Сверка договоров (Contracts)
|
||||||
|
|
||||||
|
Проект автоматизированной обработки и сверки договоров с помощью ИИ.
|
||||||
|
|
||||||
|
## Задача
|
||||||
|
|
||||||
|
Разбирать спецификации договоров и допников (docx/pdf) в структурированный вид,
|
||||||
|
отслеживать изменения между версиями, восстанавливать историю договора во времени.
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Папка | Что |
|
||||||
|
|---|---|
|
||||||
|
| `contracts-app/` | Flask-сервис (основной код) |
|
||||||
|
| `dogovora/` | Примеры договоров и допников |
|
||||||
|
| `FILES/` | Заметки, ключи LLM |
|
||||||
|
|
||||||
|
## Конфиденциальность
|
||||||
|
|
||||||
|
Данные договоров строго конфиденциальны. LLM — только собственная модель.
|
||||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,282 @@
|
|||||||
|
# История проекта Contracts (Сверка договоров)
|
||||||
|
|
||||||
|
## 2026-06-13 — Сессия #1: Инициация и развёртывание
|
||||||
|
|
||||||
|
### Участники
|
||||||
|
- Сергей Мищук — постановщик задачи
|
||||||
|
- Владимир Крупский — тех. консультация
|
||||||
|
- Наиль Тазетдинов — разработка
|
||||||
|
- GitHub Copilot (DeepSeek V4 Pro) — AI-ассистент
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Задача (из fromTelega.md)
|
||||||
|
|
||||||
|
**Сверка договоров** — автоматизированная обработка договоров и допников:
|
||||||
|
|
||||||
|
1. Разобрать спецификации (docx/pdf) до структурированного вида
|
||||||
|
2. Собрать кумулятивный статус договора по цепочке допников (во времени)
|
||||||
|
3. (опционально) Сопоставить артикулы с каталогом услуг через LLM
|
||||||
|
|
||||||
|
**Ограничения:**
|
||||||
|
- Данные строго конфиденциальны
|
||||||
|
- LLM — только своя модель
|
||||||
|
- Исключения не фантазировать, отмечать как нерешаемые
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Решения по технологиям
|
||||||
|
|
||||||
|
| Компонент | Выбор | Причина |
|
||||||
|
|---|---|---|
|
||||||
|
| Язык | Python 3.12 | python-docx, psycopg2, requests — лучшая экосистема для docx + БД + LLM |
|
||||||
|
| Фреймворк | Flask | Платформа поддерживает, шаблон Baldurs-Gate-test |
|
||||||
|
| БД | PostgreSQL | Существующий кластер postgresqlk8s |
|
||||||
|
| LLM | aillm.ru (120B) | Ключ sk-ucI5YvOticoOQK9ujK5m9Q, резервный DeepSeek Flash |
|
||||||
|
| Деплой | pythonk8s.services.ngcloud.ru | Платформа Flask-сервисов |
|
||||||
|
| Репозиторий | gitea.services.ngcloud.ru/Nail/contracts-app.git | Публичный, мастер-ветка |
|
||||||
|
|
||||||
|
**Отклонённые варианты:**
|
||||||
|
- Node.js — слабый парсинг docx (mammoth → HTML)
|
||||||
|
- Lucee/CFML — нет инструментов для docx/LLM
|
||||||
|
- SQLite — не подходит для облачного сервиса
|
||||||
|
- 3060 (личный сервер) — это личное, не для сервиса
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
**Принцип: НЕ МОНОЛИТИТЬ. Независимые слои = отдельные API.**
|
||||||
|
|
||||||
|
```
|
||||||
|
contracts-app/
|
||||||
|
├── Dockerfile
|
||||||
|
├── requirements.txt
|
||||||
|
├── .env.example
|
||||||
|
└── site/
|
||||||
|
├── app.py ← главный, собирает слои (класс ContractsApp)
|
||||||
|
├── db.py ← слой БД (connect, query)
|
||||||
|
├── test_routes.py ← слой /test (Blueprint) — мост к БД извне
|
||||||
|
├── static/css/
|
||||||
|
└── templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Слои (текущие)
|
||||||
|
|
||||||
|
| Слой | Файл | Что делает |
|
||||||
|
|---|---|---|
|
||||||
|
| Ядро | `app.py` | Класс ContractsApp, регистрирует Blueprint'ы |
|
||||||
|
| БД | `db.py` | `connect()` и `query(sql, params)` |
|
||||||
|
| Тест | `test_routes.py` | Blueprint `/test` — статус БД, список таблиц, SQL |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Конфигурация (production)
|
||||||
|
|
||||||
|
```json
|
||||||
|
// startupConfiguration
|
||||||
|
{ "resourceRealm": "k8s-4-ext-nubes-ru" }
|
||||||
|
|
||||||
|
// clusterConfiguration
|
||||||
|
{ "cpu": "500", "memory": "1024", "replicas": "1" }
|
||||||
|
|
||||||
|
// accessConfiguration
|
||||||
|
{ "domain": "contractor" }
|
||||||
|
|
||||||
|
// appConfiguration
|
||||||
|
{
|
||||||
|
"gitPath": "https://gitea.services.ngcloud.ru/Nail/contracts-app.git",
|
||||||
|
"version": "3.12",
|
||||||
|
"healthPath": "/health"
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonEnv
|
||||||
|
{
|
||||||
|
"DB_HOST": "postgresqlk8s-master.xxx.svc.cluster.local",
|
||||||
|
"DB_NAME": "contracts",
|
||||||
|
"DB_PASS": "xnm9KHLibBvT5lYuQzaBVFPYJQHfTxbS4YeEWMcFvtXuXfLaXvgogqJ6uW0nbBUB",
|
||||||
|
"DB_PORT": "5432",
|
||||||
|
"DB_USER": "constracts",
|
||||||
|
"DB_SSLMODE": "disable"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **DB_HOST** всё ещё `xxx` — нужно заменить на реальный:
|
||||||
|
`postgresqlk8s-master.60bdf3e3-5087-41ff-b760-fe6ea544a80e.svc.cluster.local`
|
||||||
|
|
||||||
|
⚠️ **DB_USER** написано `constracts` (опечатка) — должно быть `contracts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Хронология деплоев
|
||||||
|
|
||||||
|
### Попытка #1 — 11:34, Таймаут health check (478 сек)
|
||||||
|
- **Причина:** `debug=True` в app.run(), жёсткие версии в requirements.txt
|
||||||
|
- **Решение:** убрал debug, смягчил версии, health → plain text
|
||||||
|
|
||||||
|
### Попытка #2 — 12:12, Под(ы) не работают (ImportError)
|
||||||
|
- **Причина:** `from . import db` — относительные импорты не работают при `python app.py` из папки site/
|
||||||
|
- **Ошибка:** `ImportError: attempted relative import with no known parent package`
|
||||||
|
- **Решение:** заменил на абсолютные: `import db`, `from test_routes import test_bp`
|
||||||
|
|
||||||
|
### Попытка #3 — 12:18, Успех ✅
|
||||||
|
- Health: OK
|
||||||
|
- DB: fail (переменные не заданы)
|
||||||
|
|
||||||
|
### Попытка #4 — 12:??, (ожидается)
|
||||||
|
- Добавлены переменные БД
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Платформа: требования Flask-сервиса
|
||||||
|
|
||||||
|
Из `flask_manual.md`:
|
||||||
|
- `requirements.txt` в корне
|
||||||
|
- Код в `site/`
|
||||||
|
- Запуск: `python app.py`
|
||||||
|
- Python: 3.12-slim
|
||||||
|
- HealthCheck: задаётся параметром
|
||||||
|
- Git: публичный доступ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API эндпоинты
|
||||||
|
|
||||||
|
| Метод | Путь | Что |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/` | Главная (HTML, статус БД) |
|
||||||
|
| GET | `/health` | Health check → "OK" |
|
||||||
|
| GET | `/test` | Статус БД + список команд |
|
||||||
|
| POST | `/test` `{"action":"tables"}` | Список таблиц |
|
||||||
|
| POST | `/test` `{"action":"sql","sql":"..."}` | Произвольный SQL |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## План дальнейших работ
|
||||||
|
|
||||||
|
1. ✅ Flask-заготовка деплоится
|
||||||
|
2. ✅ Слои db.py + test_routes
|
||||||
|
3. ✅ Подключить БД (переменные заданы)
|
||||||
|
4. ✅ Создать базу contracts (через /test createdb)
|
||||||
|
5. ✅ Парсер docx/pdf/doc/zip — parser.py
|
||||||
|
6. ⬜ Загрузка документов в БД (contract_docs)
|
||||||
|
7. ⬜ LLM-нормализация строк
|
||||||
|
8. ⬜ Схема таблиц (contracts, spec_rows, spec_history)
|
||||||
|
9. ⬜ DIFF допников, кумулятивная история
|
||||||
|
10. ⬜ Fuzzy-match с каталогом услуг
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Хронология деплоев (полная)
|
||||||
|
|
||||||
|
### Попытка #1 — 11:34, Таймаут health check (478 сек)
|
||||||
|
- **Причина:** `debug=True` в app.run(), жёсткие версии в requirements.txt
|
||||||
|
- **Решение:** убрал debug, смягчил версии, health → plain text
|
||||||
|
|
||||||
|
### Попытка #2 — 12:12, Под(ы) не работают (ImportError)
|
||||||
|
- **Причина:** `from . import db` — относительные импорты не работают при `python app.py` из папки site/
|
||||||
|
- **Ошибка:** `ImportError: attempted relative import with no known parent package`
|
||||||
|
- **Решение:** заменил на абсолютные: `import db`, `from test_routes import test_bp`
|
||||||
|
|
||||||
|
### Попытка #3 — 12:18, Успех, но БД fail
|
||||||
|
- Health: OK
|
||||||
|
- DB: fail — переменные не заданы
|
||||||
|
|
||||||
|
### Попытка #4 — ~12:31, БД fail — password auth failed
|
||||||
|
- Хост резолвится (10.102.125.70), порт доступен
|
||||||
|
- Ошибка: `password authentication failed for user "contracts"`
|
||||||
|
- Пароль от ipwhitelist не подходит для пользователя contracts
|
||||||
|
|
||||||
|
### Попытка #5 — 12:35, db.connect() возвращает ошибки
|
||||||
|
- Улучшена диагностика — теперь /test показывает точную ошибку
|
||||||
|
|
||||||
|
### Попытка #6 — ~12:38, БД fail — database "contracts" does not exist
|
||||||
|
- Аутентификация прошла (пароль исправлен)
|
||||||
|
- База не существует
|
||||||
|
|
||||||
|
### Попытка #7 — 12:41, добавлен /test createdb
|
||||||
|
- Создан endpoint для создания БД через API
|
||||||
|
|
||||||
|
### Попытка #8 — 12:49, БАЗА СОЗДАНА ✅
|
||||||
|
- `POST /test {"action":"createdb"}` → `DB 'contracts' created`
|
||||||
|
- `/test` → `db: "ok"`
|
||||||
|
- Таблицы: только pg_stat_* (служебные)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Решения по хранению (принято)
|
||||||
|
|
||||||
|
- ❌ S3 / приватная репа / файловая система — данные уходят за пределы облака
|
||||||
|
- ❌ SQLite — не подходит для облачного сервиса
|
||||||
|
- ✅ **PostgreSQL BYTEA + JSONB** — всё внутри кластера, без внешних зависимостей
|
||||||
|
- `contract_docs` — исходные файлы (original_bytes BYTEA) + сырой парсинг (parsed_json JSONB)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Парсер (parser.py)
|
||||||
|
|
||||||
|
Принцип: **ничего не фильтровать, не терять ни символа**.
|
||||||
|
|
||||||
|
```
|
||||||
|
docx ──▶ python-docx ──▶ elements: [{paragraph, style, text}, {table, rows}, ...]
|
||||||
|
.doc ──▶ libreoffice ──▶ docx ──▶ python-docx
|
||||||
|
pdf ──▶ pdfplumber ──▶ страницы → текст + таблицы
|
||||||
|
zip ──▶ zipfile ──▶ рекурсивно parse() каждый файл
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат: полный слепок документа → в БД → дальше LLM разбирает.
|
||||||
|
|
||||||
|
Протестирован на реальном примере (спецификация-XXX001-03700.docx):
|
||||||
|
- 12 элементов (4 таблицы + 8 параграфов)
|
||||||
|
- Все стили сохранены
|
||||||
|
- Ничего не потеряно
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущая структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
contracts-app/
|
||||||
|
├── Dockerfile
|
||||||
|
├── requirements.txt ← flask, gunicorn, python-docx, requests, psycopg2-binary, python-dotenv, pdfplumber
|
||||||
|
├── .env.example
|
||||||
|
├── README.md
|
||||||
|
└── site/
|
||||||
|
├── __init__.py
|
||||||
|
├── app.py ← класс ContractsApp, сборка слоёв
|
||||||
|
├── db.py ← слой БД: connect(), query(), _pg_connect(), ensure_db()
|
||||||
|
├── test_routes.py ← слой /test: статус, tables, sql, createdb
|
||||||
|
├── parser.py ← слой парсера: parse() для docx/pdf/doc/zip
|
||||||
|
├── static/css/
|
||||||
|
└── templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Конфигурация (актуальная)
|
||||||
|
|
||||||
|
```json
|
||||||
|
// clusterConfiguration
|
||||||
|
{ "cpu": "500", "memory": "1024", "replicas": "1" }
|
||||||
|
|
||||||
|
// accessConfiguration
|
||||||
|
{ "domain": "contractor" }
|
||||||
|
// URL: https://contractor.pythonk8s.services.ngcloud.ru
|
||||||
|
|
||||||
|
// appConfiguration
|
||||||
|
{
|
||||||
|
"gitPath": "https://gitea.services.ngcloud.ru/Nail/contracts-app.git",
|
||||||
|
"version": "3.12",
|
||||||
|
"healthPath": "/health"
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonEnv
|
||||||
|
{
|
||||||
|
"DB_HOST": "postgresqlk8s-master.60bdf3e3-5087-41ff-b760-fe6ea544a80e.svc.cluster.local",
|
||||||
|
"DB_NAME": "contracts",
|
||||||
|
"DB_PASS": "****",
|
||||||
|
"DB_PORT": "5432",
|
||||||
|
"DB_USER": "contracts",
|
||||||
|
"DB_SSLMODE": "disable"
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user