215 lines
18 KiB
Markdown
215 lines
18 KiB
Markdown
# Контракт-чек
|
||
|
||
LLM-сервис скрининга рисков в договорах по ГК РФ / ГК РБ.
|
||
Telegram-бот + web-аутентификация (email/password, JWT-пара; пасскеи и магические ссылки — passwordless) + B2B API + биллинг (ЮKassa: подписки, пакеты кредитов, авто-возвраты) сейчас; web-SPA — позже.
|
||
|
||
Pipeline: `PDF/DOCX/RTF/TXT/CSV/изображения (pymupdf / mammoth / tesseract OCR) → текст → чанки →
|
||
прескрин (гибридная эвристика + LLM: метаданные договора, роутинг) → LLM-анализ
|
||
(Ollama Cloud или YandexGPT, json-schema + repair-loop) → markdown-отчёт`
|
||
с цитатами, ссылкой на пункт и дисклеймером «не заменяет юриста».
|
||
|
||
## Документы
|
||
|
||
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — **единственный источник истины** для production-рефакторинга. Hexagonal-архитектура, RabbitMQ-конвейер, схемы БД, очередь/ретраи/DLQ, конфиг, deploy, observability.
|
||
- [`docs/DEPLOY.md`](docs/DEPLOY.md) — руководство по развёртыванию: локально, Docker Compose, VPS, seed-token, backup, troubleshooting.
|
||
- [`docs/BUSINESS_IDEA.md`](docs/BUSINESS_IDEA.md) — идея и бизнес-модель.
|
||
- [`docs/TICKETS.md`](docs/TICKETS.md) — тикеты реализации со статусами.
|
||
|
||
## Архитектура (одна строка)
|
||
|
||
Hexagonal (ports & adapters). Ядро `contract_check.core` владеет всем состоянием
|
||
(Postgres/MinIO/RabbitMQ/LLM/кредиты/биллинг). Семь сервисов собираются из него:
|
||
|
||
```
|
||
Telegram ──► bot (aiogram, HTTP-only) ──HTTP──► api (FastAPI) ──publish──► RabbitMQ
|
||
владеет: PG, MinIO, │
|
||
Redis, кредитами ▼
|
||
extract.q ──► worker-extract
|
||
(pymupdf/mammoth/tesseract, CPU) │
|
||
▼ publish
|
||
prescreen.q ──► worker-prescreen
|
||
(гибридные метаданные + роутинг) │
|
||
┌──────────────────────────────────────┤
|
||
▼ deep_analysis manual_review
|
||
analyze.q ──► worker-analyze │
|
||
(LLM, I/O) → Report │
|
||
▼
|
||
notify.q ──► worker-notify (SMTP: password reset, magic link и др.)
|
||
```
|
||
|
||
- **api** — единственный писатель для пользовательских мутаций (upload → reserve credit → MinIO → publish `DocumentUploaded`).
|
||
- **worker-extract** — CPU: достаёт текст (PDF/DOCX/RTF/TXT/CSV, OCR для изображений; детект формата через python-magic), грузит извлечённый текст в MinIO, публикует `PrescreenRequested` (если `PRESCREEN_ENABLED=true`) или сразу `DocumentExtracted` в `analyze.q`.
|
||
- **worker-prescreen** — гибридное извлечение метаданных договора (Stage 1 эвристика + Stage 2 LLM при низкой уверенности), роутинг: `deep_analysis` → публикует `AnalyzeRequested` в `analyze.q`; `manual_review` — терминальный статус; `auto_approve` — записывает лёгкий `Report`, `status=done`. Пишет в `prescreen_results`.
|
||
- **worker-analyze** — I/O: LLM-анализ по чек-листу, валидация/repair, сохраняет `Report`, `status=done`.
|
||
- **worker-notify** — доставка email-уведомлений (восстановление пароля и др.) через SMTP; без `SMTP_HOST` — dev-логгер.
|
||
- **worker-billing** — таймерный планировщик биллинга (без MQ): продления подписок (счета за 3 дня до конца периода), expiry/past_due-переходы, сверка pending-счетов старше 15 минут через ЮKassa API.
|
||
- **bot** — адаптер: HTTP-клиент к api, **не импортирует** core.db/s3/llm/mq (граница проверяется тестом `tests/unit/test_bot_boundary.py`). Может работать на отдельном сервере — см. `docs/DEPLOY.md` §14.
|
||
|
||
## Структура репозитория
|
||
|
||
```
|
||
src/contract_check/
|
||
core/ # общий домен (импортируется каждым сервисом)
|
||
config.py logging.py telemetry.py sentry.py metrics.py errors.py
|
||
credits.py tokens.py api_keys.py rate_limit.py redis_client.py
|
||
auth.py auth_refresh.py auth_refresh_key.py passkeys.py
|
||
db/ models.py session.py enums.py repositories/
|
||
mq/ topology.py publisher.py consumer.py messages.py
|
||
s3/ port.py minio_storage.py
|
||
llm/ port.py factory.py ollama_cloud.py yandex_gpt.py prescreen.py errors.py
|
||
billing/ port.py yookassa.py errors.py quota.py fulfillment.py refunds.py
|
||
extraction/ port.py factory.py formats.py + adapters/
|
||
(pdf_pymupdf, docx_mammoth, rtf_striprtf, txt_chardet, ocr_tesseract)
|
||
analysis/ extractor.py chunker.py checklist.py report_schema.py ocr.py analyzer.py
|
||
notifications/ transport.py publisher.py # SMTP + dev-log
|
||
security/ passwords.py # argon2
|
||
api/ # FastAPI-образ
|
||
app.py deps.py middleware.py services.py __main__.py
|
||
admin/ # серверный UI по /admin (login, users, счета, возвраты, hold)
|
||
billing.py users.py auth.py router.py templating.py templates/
|
||
billing/ # token-gated HTML pay-страницы (/pay/{id})
|
||
routes/ # префикс /api/v1
|
||
__init__.py health.py documents.py reports.py me.py metrics.py b2b.py billing.py webhooks.py
|
||
auth/ # Telegram, email/password, passkeys, magic links
|
||
__init__.py telegram.py password.py passkeys.py magic_links.py support.py
|
||
schemas/ # Pydantic-схемы запросов/ответов
|
||
auth.py b2b.py billing.py common.py documents.py me.py profile.py reports.py
|
||
worker_extract/ # CPU-образ (pymupdf + mammoth + tesseract)
|
||
consumer.py handler.py extract_document.py __main__.py
|
||
worker_prescreen/ # прескрин-образ (гибрид: эвристика + LLM)
|
||
consumer.py handler.py router.py config.py
|
||
extractor.py extractor_heuristic.py extractor_llm.py extractor_hybrid.py
|
||
worker_analyze/ # I/O-образ (LLM provider)
|
||
consumer.py handler.py __main__.py
|
||
worker_notify/ # email-уведомления (aiosmtplib)
|
||
consumer.py handler.py __main__.py
|
||
worker_billing/ # планировщик биллинга (продления, expiry, сверка)
|
||
scheduler.py __main__.py
|
||
bot/ # aiogram-адаптер (самый «тощий» образ: только core.logging)
|
||
client.py config.py handlers.py rate_limit.py __main__.py
|
||
docs/ # документация проекта (README остаётся в корне)
|
||
srv/ # Dockerfile-ы (один на сервис, deps заточены)
|
||
api/ worker-extract/ worker-prescreen/ worker-analyze/ worker-notify/ worker-billing/ bot/
|
||
migrations/ # alembic (async): 0001_initial … 0011_user_profiles_billing
|
||
tests/
|
||
conftest.py
|
||
unit/ chunker, extractor, extraction_factory/adapters, llm_ollama_cloud,
|
||
llm_yandex_gpt, llm_prescreen_extraction, prescreen_extractor(+heuristic/
|
||
hybrid/llm), prescreen_router, credits, messages, rate_limit,
|
||
checklist_report, auth, web_auth, passkeys, extract_handler,
|
||
bot_client, bot_boundary, refunds, worker_billing
|
||
integration/ upload_pipeline, extract_worker, prescreen_worker, analyze_worker,
|
||
b2b_api, credits_db, auth_flow, passkeys_magic_link, admin_panel,
|
||
profile_api, b2b_profile, billing_checkout, webhook_yookassa,
|
||
pay_page, subscription_purchase, quota_reservation, refunds,
|
||
admin_billing
|
||
Makefile # повседневные команды (make help)
|
||
docker-compose.yml # default = инфра; --profile services = стек; --profile edge = nginx+certbot
|
||
pyproject.toml # hatchling + PEP 735 dependency-groups (db/mq/s3/obs/api/extract/prescreen/analyze/notify/billing/bot/dev)
|
||
.env.example # полный список env (см. docs/ARCHITECTURE.md §11)
|
||
```
|
||
|
||
## Быстрый старт
|
||
|
||
### Локально (разработка)
|
||
|
||
```bash
|
||
uv sync --group dev # все группы для локальной разработки
|
||
cp .env.example .env # впишите OLLAMA_* / YANDEXGPT_* / JWT_SECRET / BOT_TOKEN
|
||
docker compose up -d # только инфра (postgres/redis/rabbitmq/minio)
|
||
uv run alembic upgrade head # миграции
|
||
uv run python -m contract_check.api # api на :8000
|
||
uv run python -m contract_check.worker_extract # воркер экстракции
|
||
uv run python -m contract_check.worker_prescreen # воркер прескрина
|
||
uv run python -m contract_check.worker_analyze # воркер анализа
|
||
uv run python -m contract_check.worker_notify # воркер уведомлений
|
||
uv run python -m contract_check.worker_billing # планировщик биллинга
|
||
uv run python -m contract_check.bot # Telegram-бот
|
||
```
|
||
|
||
### Через Docker Compose / Make
|
||
|
||
```bash
|
||
cp .env.example .env # заполнить секреты (DB, Rabbit, MinIO, LLM, BOT_TOKEN, JWT_SECRET, ...)
|
||
docker compose up -d # только инфра с healthchecks
|
||
docker compose --profile services up -d --build # + api, worker-extract, worker-prescreen, worker-analyze, worker-notify, worker-billing (без бота)
|
||
docker compose --profile bot up -d --build # + bot (можно запускать отдельно или на другом хосте)
|
||
```
|
||
|
||
Или через `make`: `make dev` (install + infra + migrate + api + workers + bot), `make help` — полный список
|
||
целей (lint, typecheck, test, seed-token, jwt-token, admin-promote, логи/шеллы сервисов и т.п.).
|
||
|
||
Профили:
|
||
- `services` — api + worker-ы (без бота).
|
||
- `bot` — Telegram-бот; можно поднять на этом же хосте или на отдельном сервере (`docs/DEPLOY.md` §14).
|
||
- `edge` — Nginx + certbot (`deploy/nginx/`, `docs/DEPLOY.md` §13).
|
||
- `obs` / `observer` — observability (Grafana/Loki/Prometheus или OpenObserve + OTel collector).
|
||
|
||
Порты на хосте (смещены, чтобы не конфликтовать): Postgres `15432`, Redis `17379`,
|
||
RabbitMQ AMQP `5672` / UI `15672`, MinIO `9000` / console `9001`, api `8000` / metrics `9100`,
|
||
worker metrics: extract `9101`, analyze `9102`, notify `9103`, prescreen `9104`, billing `9105`, edge `80`/`443`.
|
||
|
||
## API (кратко)
|
||
|
||
JSON-роуты под `/api/v1`; серверный admin UI — по `/admin/*` (FastAPI + Jinja2 + HTMX). Auth зависит от роута:
|
||
|
||
- **Пользовательские роуты** (bot / web / Mini App) — `Authorization: Bearer <user_jwt>`. Telegram-JWT выдаётся через `/api/v1/auth/telegram/*`; web — JWT-пара (access + refresh, refresh отзывается через Redis); passwordless (пасскей / магическая ссылка) — одиночный access JWT.
|
||
- **Адаптер-level** (только `/api/v1/auth/telegram/bot`) — `Authorization: Bearer <service_token>`.
|
||
- **B2B** — `X-API-Key`. Управление B2B-ключами требует пользовательский JWT.
|
||
- **Health/metrics** — без auth.
|
||
- **/admin** — HttpOnly cookie с user JWT + `users.role = admin` (или `ADMIN_REQUIRED_ROLE`).
|
||
|
||
| Метод | Путь | Auth | Назначение |
|
||
|---|---|---|---|
|
||
| GET | `/healthz`, `/readyz`, `/metrics` | — | liveness / readiness / Prometheus |
|
||
| POST | `/api/v1/auth/telegram/bot` | service token | бот меняет verified `telegram_id` на JWT |
|
||
| POST | `/api/v1/auth/telegram/web` | — | Telegram Login Widget → JWT |
|
||
| POST | `/api/v1/auth/telegram/miniapp` | — | Mini App `initData` → JWT |
|
||
| POST | `/api/v1/auth/register` | — | регистрация email/password → JWT-пара |
|
||
| POST | `/api/v1/auth/login` | — | вход email/password → JWT-пара |
|
||
| POST | `/api/v1/auth/logout` | — | отзыв refresh-токена |
|
||
| POST | `/api/v1/auth/forgot-password` | — | reset-токен (хранится хэшем) + уведомление через notify.q |
|
||
| POST | `/api/v1/auth/reset-password` | — | задать новый пароль, отозвать все refresh |
|
||
| POST | `/api/v1/auth/passkeys/register/start`, `.../finish` | user JWT | регистрация пасскея (WebAuthn, challenge в Redis) |
|
||
| POST | `/api/v1/auth/passkeys/authenticate/start`, `.../finish` | — | вход по пасскею (discoverable) → access JWT |
|
||
| GET/DEL | `/api/v1/auth/passkeys`, `/api/v1/auth/passkeys/{id}` | user JWT | список / удаление пасскеев |
|
||
| POST | `/api/v1/auth/magic-link/request` | — | одноразовая ссылка входа на email (через notify.q) |
|
||
| POST | `/api/v1/auth/magic-link/verify` | — | обмен токена из письма на access JWT |
|
||
| GET | `/api/v1/auth/me`, `/api/v1/auth/me/permissions` | user JWT | introspect JWT / права |
|
||
| POST | `/api/v1/documents` | user JWT | multipart upload → reserve credit → MinIO → publish → `202 {document_id, correlation_id}` |
|
||
| GET | `/api/v1/reports/{document_id}` | user JWT | поллинг: `{status, stage}` или готовый `200 {markdown, findings, ...}` |
|
||
| GET | `/api/v1/me` | user JWT | профиль + `credits_left` |
|
||
| GET/PATCH | `/api/v1/me/profile` | user JWT | пассивные настройки профиля (язык, TZ, уведомления, дашборд) |
|
||
| GET | `/api/v1/me/overview` | user JWT | дашборд: документы, кредиты, план/квота, `billing_hold` |
|
||
| GET | `/api/v1/me/documents` | user JWT | список документов пользователя |
|
||
| POST | `/api/v1/me/telegram`, `/api/v1/me/password` | user JWT | привязка Telegram / смена пароля |
|
||
| GET | `/api/v1/billing/plans` | — | каталог тарифов + цена за документ |
|
||
| POST | `/api/v1/billing/checkout` | user JWT | топ-ап кредитов или покупка подписки → `201 {invoice_id, confirmation_url}` |
|
||
| GET | `/api/v1/billing/invoices`, `/api/v1/billing/invoices/{id}` | user JWT | история / свой счёт |
|
||
| GET | `/api/v1/billing/subscription` | user JWT | текущая подписка (active/past_due) |
|
||
| POST | `/api/v1/billing/subscriptions/autorenew` | user JWT | вкл/выкл автопродление (по умолчанию OFF) |
|
||
| POST | `/api/v1/billing/refund` | user JWT | авто-возврат по правилу D10; `409` если не возвращаем |
|
||
| POST | `/api/v1/webhooks/yookassa` | Basic (shopId:secret) | уведомления ЮKassa; платёж перезапрашивается через REST |
|
||
| GET | `/pay/{invoice_id}?token=…` | подписанный JWT | HTML-страница статуса счёта после оплаты |
|
||
| GET/PUT | `/api/v1/b2b/profile` | `X-API-Key` | настройки профиля партнёра (без биллинга) |
|
||
| POST | `/api/v1/analyze` | `X-API-Key` | B2B: анализ документа |
|
||
| GET | `/api/v1/b2b/reports/{id}`, `/api/v1/b2b/usage` | `X-API-Key` | B2B: отчёт / usage |
|
||
| POST/GET | `/api/v1/b2b/keys`, `/api/v1/b2b/keys/{id}/revoke`, `.../usage` | user JWT | управление B2B-ключами |
|
||
| GET/POST | `/admin/*` | admin cookie | login/logout + пользователи (list, create, edit, ban, role, credits, verify-telegram), счета (`/admin/invoices`), возвраты, снятие billing-hold, gift-подписки |
|
||
|
||
Биллинг-флаги: `PLANS_ENABLED` (квоты подписок; `false` = только кредиты),
|
||
`YOOKASSA_ENABLED` (платежи; `false` → мутации биллинга `503`, каталог читается).
|
||
Детали: `docs/ARCHITECTURE.md §8a`, настройка ЮKassa — `docs/DEPLOY.md §4a`.
|
||
|
||
Полная спецификация роутов (схемы запросов/ответов, коды ошибок) —
|
||
[`src/contract_check/api/routes/README.md`](src/contract_check/api/routes/README.md);
|
||
высокоуровневая поверхность — `docs/ARCHITECTURE.md §15` (актуализируется).
|
||
|
||
## Проверки (DoD)
|
||
|
||
```bash
|
||
uv run ruff check src tests && uv run ruff format --check src tests # lint (или make lint)
|
||
uv run ty check src # типы (или make typecheck)
|
||
uv run pytest -m "not integration" -q # unit, быстро
|
||
uv run pytest -m integration -q # интеграционные (нужны контейнеры)
|
||
```
|