diff --git a/README.md b/README.md index 3a104cb..a0b5164 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ Telegram ──► bot (aiogram, HTTP-only) ──HTTP──► api (FastAPI) ``` - **api** — единственный писатель для пользовательских мутаций (upload → reserve credit → MinIO → publish `DocumentUploaded`). -- **worker-extract** — CPU: достаёт текст (PDF/DOCX/RTF/TXT/CSV, OCR для изображений; детект формата через python-magic), грузит markdown в MinIO, публикует `DocumentExtracted`. -- **worker-prescreen** — гибридное извлечение метаданных договора (Stage 1 эвристика + Stage 2 LLM при низкой уверенности), роутинг: `deep_analysis` → analyze.q / `manual_review` / `auto_approve` → report.completed. Пишет в `prescreen_results`. +- **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-логгер. - **bot** — адаптер: HTTP-клиент к api, **не импортирует** core.db/s3/llm/mq (граница проверяется тестом `tests/unit/test_bot_boundary.py`). @@ -52,23 +52,28 @@ Telegram ──► bot (aiogram, HTTP-only) ──HTTP──► api (FastAPI) ``` src/contract_check/ __main__.py # указывает на prototype (stage-0 CLI сохранён) - 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 - 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 - extraction/ port.py factory.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 + 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 + 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; позже — подписки) - routes/ health.py documents.py reports.py me.py metrics.py auth.py b2b.py + routes/ # префикс /api/v1 + __init__.py health.py documents.py reports.py me.py metrics.py b2b.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 common.py documents.py me.py reports.py worker_extract/ # CPU-образ (pymupdf + mammoth + tesseract) consumer.py handler.py extract_document.py __main__.py worker_prescreen/ # прескрин-образ (гибрид: эвристика + LLM) @@ -84,7 +89,7 @@ src/contract_check/ docs/ # документация проекта (README остаётся в корне) srv/ # Dockerfile-ы (один на сервис, deps заточены) api/ worker-extract/ worker-prescreen/ worker-analyze/ worker-notify/ bot/ prototype/ -migrations/ # alembic (async): 0001_initial … 0009_user_name +migrations/ # alembic (async): 0001_initial … 0010_passkeys_magic_links tests/ conftest.py unit/ chunker, extractor, extraction_factory/adapters, llm_ollama_cloud, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 97fd902..df6cb5f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1274,7 +1274,7 @@ Three JSON auth modes: Health/metrics exempt from auth. -### Auth endpoints (`api/routes/auth.py`) +### Auth endpoints (`api/routes/auth/`) — `telegram.py`, `password.py`, `passkeys.py`, `magic_links.py` | Method | Path | Auth | Behavior | |---|---|---|---| diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index bf8d461..9018892 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -3,8 +3,9 @@ > **Примечание:** этот документ описывает исходную «ленивую» архитектуру с arq+Redis > (очередь), Selectel S3 (хранилище) и единым Docker-образом с `MODE=api|worker|bot`. > **Этап 0 (прототип) актуален.** Этапы 1+ superseded [`ARCHITECTURE.md`](ARCHITECTURE.md), -> где реализована production-архитектура: RabbitMQ-конвейер, MinIO, два worker-а, -> aiogram-бот, B2B API, 5 Dockerfile-ов в `srv/`, dependency-groups (PEP 735). +> где реализована production-архитектура: RabbitMQ-конвейер, MinIO, 4 worker-а +> (extract, prescreen, analyze, notify), aiogram-бот, B2B API, 7 Dockerfile-ов в `srv/`, +> dependency-groups (PEP 735). > Принцип: ленивая архитектура. Каждый этап — минимальный работающий > срез. Никаких абстракций «на потом». K8s, RabbitMQ, микросервисы — diff --git a/docs/PHASES_2_PLUS_ROADMAP.md b/docs/PHASES_2_PLUS_ROADMAP.md index 63ed1bc..546c587 100644 --- a/docs/PHASES_2_PLUS_ROADMAP.md +++ b/docs/PHASES_2_PLUS_ROADMAP.md @@ -10,7 +10,7 @@ |---|---|---| | Phase 0 | Needle/RustFS spikes | Done (`docs/SPIKE_PHASE0.md`) | | Phase 1 | Extraction layer refactor | Done (`docs/PHASE1_HANDOFF.md`) | -| Phase 2 | Prescreen stage | **Planned below** | +| Phase 2 | Prescreen stage | **Done** (`worker_prescreen/`, migrations `0006_prescreen.py` … `0008_add_manual_review_status.py`) | | Phase 3 | Storage modernization / RustFS watch | **Planned below** | | Follow-up | Admin/web, payments, heavy OCR | **Backlog** | diff --git a/docs/TICKETS.md b/docs/TICKETS.md index be5b027..869b0eb 100644 --- a/docs/TICKETS.md +++ b/docs/TICKETS.md @@ -1,10 +1,12 @@ # Тикеты реализации «Контракт-чек» (production refactor) > **Актуальная архитектура:** `ARCHITECTURE.md` (supersedes `IMPLEMENTATION_PLAN.md` для этапов 1+). -> Состояние кода на момент синхронизации: реализованы `api/` (вкл. B2B-роуты), `core/`, -> `worker_extract/`, `worker_analyze/`, `bot/`, `prototype/`; все 5 Dockerfile-ов в `srv/`; -> `docker-compose.yml` полностью разводит профиль `services`; миграции `0001_initial` + `0002_api_keys`; -> unit-тесты зелёные. DoD: `ruff check .`, `mypy src`, `pytest` зелёные; `.env.example` актуален. +> Состояние кода на момент синхронизации: реализованы `api/` (вкл. B2B-роуты, web-auth, passkeys, magic links), +> `core/`, `worker_extract/`, `worker_prescreen/`, `worker_analyze/`, `worker_notify/`, `bot/`, `prototype/`; +> 7 Dockerfile-ов в `srv/`; `docker-compose.yml` разводит профили `services` и `edge`; +> миграции `0001_initial` … `0010_passkeys_magic_links`; unit- и интеграционные тесты зелёные. +> DoD: `ruff check src tests`, `ruff format --check src tests`, `uv run ty check src`, `pytest` зелёные; +> `.env.example` актуален. > > Статусы: `todo` / `in_progress` / `done` / `blocked`. @@ -13,21 +15,22 @@ ## Аудит реализации (текущее состояние) > Проверено: код собирается (`pyproject.toml` + `uv.lock`), миграции накатываются, -> `api/` стартует, unit-тесты проходят (58 тестов зелёные). +> `api/` стартует, unit-тесты проходят (≈80 тестов зелёные), интеграционные тесты проходят на поднятой инфраструктуре. | Компонент | Статус | Доказательство / пробел | |-----------|--------|---------------------------| | Stage 0 prototype | done | `src/contract_check/prototype/` работает; `tests/unit/test_checklist_report.py`, `test_chunker.py`, `test_extractor.py` зелёные. | -| `core/` — общий домен | done | `db/`, `mq/`, `s3/`, `llm/`, `analysis/`, `credits.py`, `tokens.py`, `api_keys.py`, `rate_limit.py`, `redis_client.py`, `config.py`, `logging.py`, `metrics.py`, `telemetry.py`, `sentry.py`. | -| Миграции / БД | done | `0001_initial.py` (6 таблиц) + `0002_api_keys.py` (`api_keys`, `api_key_requests`). | -| `api/` — FastAPI ядро | done | `POST /api/v1/documents`, `GET /api/v1/reports/{id}`, `GET /api/v1/documents/{id}`, `GET /api/v1/me`, `/healthz`, `/readyz`, `/metrics`; user JWT auth via `/api/v1/auth/telegram/*`; reserve-on-enqueue (`services.py`). | -| `worker_extract/` | done | `consumer.py`/`handler.py`/`extract_document.py`/`__main__.py`: consume `DocumentUploaded` → MinIO dl → extract/OCR → `.txt` upload → publish `DocumentExtracted`; failure-class + refund. | +| `core/` — общий домен | done | `db/` (models, session, enums, repositories), `mq/`, `s3/`, `llm/`, `analysis/`, `extraction/`, `notifications/`, `security/`, `credits.py`, `tokens.py`, `api_keys.py`, `rate_limit.py`, `redis_client.py`, `config.py`, `logging.py`, `metrics.py`, `telemetry.py`, `sentry.py`, `passkeys.py`. | +| Миграции / БД | done | `0001_initial.py` (6 таблиц) … `0010_passkeys_magic_links.py` (passkeys, magic links, user name). | +| `api/` — FastAPI ядро | done | `POST /api/v1/documents`, `GET /api/v1/reports/{id}`, `GET /api/v1/me`, `/healthz`, `/readyz`, `/metrics`; user JWT auth (`/api/v1/auth/telegram/*`, `/api/v1/auth/{register,login,...}`, passkeys, magic links); B2B `/api/v1/analyze`; `/admin/*`; reserve-on-enqueue (`services.py`). | +| `worker_extract/` | done | `consumer.py`/`handler.py`/`extract_document.py`/`__main__.py`: consume `DocumentUploaded` → MinIO dl → extract/OCR → `.txt` upload → publish `PrescreenRequested` (или `DocumentExtracted`, если прескрин выключен); failure-class + refund. | +| `worker_prescreen/` | done | `consumer.py`/`handler.py`/`__main__.py` + `extractor*.py`/`router.py`/`config.py`: consume `PrescreenRequested` → гибридная экстракция метаданных → роутинг `deep_analysis`/`manual_review`/`auto_approve`; публикация `AnalyzeRequested` или терминальный статус/лёгкий отчёт. | | `worker_analyze/` | done | `consumer.py`/`handler.py`/`__main__.py`: consume `DocumentExtracted` → LLM → Report → `status=done`; refund-on-DLQ по политике. | +| `worker_notify/` | done | `consumer.py`/`handler.py`/`__main__.py`: consume `NotificationMessage` → SMTP (password reset, magic link) или dev-лог при пустом `SMTP_HOST`. | | `bot/` — Telegram adapter | done | `client.py`/`config.py`/`handlers.py`/`__main__.py`: `/start`, upload→`POST /documents`, poll→deliver; граница импортов проверяется `tests/unit/test_bot_boundary.py`. | -| Dockerfile-ы | done | `srv/{api,worker-extract,worker-analyze,bot,prototype}/Dockerfile` — все 5 (deps-группы PEP 735 заточены на сервис). | -| Docker Compose | done | Инфра (default) + профиль `services` (api, worker-extract, worker-analyze, bot) с `depends_on: service_healthy`. Профили `obs`/`edge` — позже. | -| Observability / edge | todo | Пром/Grafana/Tempo/OTel-collector/Nginx/certbot — не развёрнуты (нет `deploy/`). | -| Stage 2 — веб + подписки | todo | React SPA, Telegram Login, recurring ЮKassa — не начаты. | +| Dockerfile-ы | done | `srv/{api,worker-extract,worker-prescreen,worker-analyze,worker-notify,bot,prototype}/Dockerfile` — все 7 (deps-группы PEP 735 заточены на сервис). | +| Docker Compose | done | Инфра (default) + профиль `services` (api, 3 worker-а, bot) + профиль `edge` (nginx+certbot) с `depends_on: service_healthy`. Профиль `obs` — позже. | +| Observability | in_progress | Prometheus-метрики (`/metrics`), Sentry, OpenTelemetry SDK — в коде. Полный стек Prom/Grafana/Tempo/OTel-collector — позже. | | Stage 3 — B2B API | done | `api/routes/b2b.py`, `core/api_keys.py`, `core/rate_limit.py`, `core/redis_client.py`, миграция `0002_api_keys.py`, `tests/integration/test_b2b_api.py`, `tests/unit/test_rate_limit.py`; `X-API-Key` auth + token-bucket rate-limit. | --- @@ -86,14 +89,14 @@ **Статус:** done · **Оценка:** L `src/contract_check/api/`: `POST /api/v1/documents`, `GET /api/v1/reports/{id}`, `GET /api/v1/me`, `/healthz`, `/metrics`. -Reserve-on-enqueue (`core/credits.py`) и user JWT auth (`core/auth.py`, `api/routes/auth.py`). +Reserve-on-enqueue (`core/credits.py`) и user JWT auth (`core/auth.py`, `api/routes/auth/`). ### T-E1-004 — Worker-extract (CPU: pymupdf + tesseract) **Статус:** done · **Оценка:** M · **Зависимости:** T-E1-001, T-E1-002 `src/contract_check/worker_extract/` (`consumer.py`/`handler.py`/`extract_document.py`/`__main__.py`): consume `DocumentUploaded` из `extract.q` → скачать blob из MinIO → -`extractor` + `ocr.ocr_pdf()` → загрузить `.txt` → publish `DocumentExtracted` в `analyze.q`. +`extractor` + `ocr.ocr_pdf()` → загрузить `.txt` → publish `PrescreenRequested` в `prescreen.q` (или `DocumentExtracted` в `analyze.q`, если прескрин выключен). Failure-classification + refund-on-DLQ. Образ `srv/worker-extract/Dockerfile`. ### T-E1-005 — Worker-analyze (I/O: LLM provider) @@ -117,8 +120,8 @@ consume `DocumentExtracted` из `analyze.q` → скачать `.txt` → chunk **Статус:** done · **Оценка:** M · **Зависимости:** T-E1-004, T-E1-005, T-E1-006 `docker-compose.yml`: default-профиль = инфра (`postgres`, `redis`, `rabbitmq`, `minio`, `minio-init`); -профиль `services` = `api`, `worker-extract`, `worker-analyze`, `bot` с `depends_on: service_healthy`. -Все 5 Dockerfile-ов в `srv/`. Профили `obs`/`edge` — позже (T-E1-009/T-E1-010). +профиль `services` = `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-notify`, `bot` с `depends_on: service_healthy`; +профиль `edge` = nginx+certbot. Все 7 Dockerfile-ов в `srv/`. Профиль `obs` — позже (T-E1-010). ### T-E1-008 — Оплата (ЮKassa) и пополнение кредитов **Статус:** todo · **Оценка:** M · **Зависимости:** T-E1-003, T-E1-006 @@ -147,7 +150,7 @@ consume `DocumentExtracted` из `analyze.q` → скачать `.txt` → chunk ### T-E2-002 — Telegram Login Widget auth **Статус:** done · **Оценка:** M · **Зависимости:** T-E1-003 -Реализовано в `core/auth.py` + `api/routes/auth.py`: `/api/v1/auth/telegram/web` и `/api/v1/auth/telegram/miniapp` проверяют HMAC-подпись Telegram и выдают тот же user JWT, что и бот. +Реализовано в `core/auth.py` + `api/routes/auth/telegram.py`: `/api/v1/auth/telegram/web` и `/api/v1/auth/telegram/miniapp` проверяют HMAC-подпись Telegram и выдают тот же user JWT, что и бот. ### T-E2-003 — React SPA (Vite) **Статус:** todo · **Оценка:** L · **Зависимости:** T-E2-002 diff --git a/src/contract_check/api/routes/README.md b/src/contract_check/api/routes/README.md index 40e4f2d..043cb3b 100644 --- a/src/contract_check/api/routes/README.md +++ b/src/contract_check/api/routes/README.md @@ -80,7 +80,7 @@ Exposition-эндпоинт Prometheus. Тело — текст в формат ## auth -Файл: [`auth.py`](auth.py). Тег: `auth`. +Файлы: [`auth/`](auth/) — `telegram.py`, `password.py`, `passkeys.py`, `magic_links.py`, `support.py`. Тег: `auth`. Три источника идентичности сходятся к одному JWT: @@ -375,7 +375,7 @@ request — тело `{ "email" }`, всегда `200 { success, message, expire | Поле | Тип | Описание | | ----- | ----------- | ------------------------------------- | -| `file`| UploadFile | Расширения: `.pdf`, `.docx` (см. `SUPPORTED_SUFFIXES`) | +| `file`| UploadFile | Расширения: `.pdf`, `.docx`, `.rtf`, `.txt`, `.csv`, `.png`, `.jpg`, `.jpeg`, `.tif`, `.tiff` (см. `core/extraction/formats.py`) | Ответ `202` (результат [`upload_and_enqueue`](../services.py)):