Files
telegram-helper-bot/.cursor/implementation-plan-features.md

429 lines
24 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План реализации фич Telegram Helper Bot
> Документ создан: 28 февраля 2025
> Ветка: `dev-*`
> Статус: План утверждён
---
## Обзор фич
1. **Пагинация заблокированных пользователей** — сортировка по дате бана, единая логика текста и кнопок
2. **Обогащение сообщений пользователей админам** — данные о пользователе при обращении в поддержку
3. **Причина бана «Последний пост»** — замена «Спам» на «Последний пост» при быстром бане из поста
4. **Похожие посты за 24ч** — проверка на дубликаты через RAG (threshold >0.9)
5. **Авто-публикация/отклонение по RAG** — задел на будущее (>0.8 publish, <0.4 decline)
6. **ML Scoring Статистика** — восстановить полный вывод (модель, примеры, device) вместо fallback (API URL, статус)
---
## Решения по уточняющим вопросам
| Вопрос | Решение |
|--------|---------|
| Пагинация | Единый источник данных, единый `items_per_page` |
| message_id при forward | Осознанный workaround (n+1). При переходе на send — использовать `returned_message.message_id` |
| RAG similar | Новый endpoint в RAG. Нужна **отдельная коллекция** для submitted-постов (не позитив/негатив) |
| Скор для авто-решений | Только `rag_score` |
| Ветки | `dev-*` |
---
## Проверка: message_id при forward
**Telegram Bot API:** `forwardMessage` возвращает объект `Message` — это **новое** сообщение в целевом чате со своим `message_id`. Telegram присваивает `message_id` в целевом чате — он не обязан быть `n+1` от исходного.
Если всё работает — возможно, бот единственный отправитель в `group_for_message`, и id часто идут подряд. Рекомендация: при переходе на `send_message` обязательно сохранять `message_id` из возвращаемого объекта.
---
## RAG: коллекция для похожих постов
- **Позитив/негатив** — примеры для скоринга модерации
- **Похожие посты** — сравнение с другими submitted-постами за 24ч
Endpoint `/similar` должен искать по **отдельной коллекции submitted-постов** (с `created_at`), а не по позитиву/негативу. Нужно:
- Новая коллекция в RAG (например, `posts_submitted`) с полями: `text`, `vector`, `created_at`, `post_id`
- Endpoint `POST /similar`: `{"text": "...", "threshold": 0.9, "hours": 24}` → список похожих постов
- При каждом suggest — добавлять пост в эту коллекцию (или вызывать endpoint RAG для индексации)
---
## Пошаговый план реализации
### Этап 0: Подготовка
| Шаг | Действие |
|-----|----------|
| 0.1 | Создать ветку `dev-N` (N — следующий номер) от `main` |
| 0.2 | Убедиться, что локально проходит `make code-quality` (или `isort`, `black`, `flake8`, `pytest`) |
---
### Этап 1: Пагинация заблокированных пользователей
| Шаг | Действие |
|-----|----------|
| 1.1 | В `BlacklistRepository`: добавить `ORDER BY created_at DESC` в `get_all_users` и `get_all_users_no_limit` |
| 1.2 | Ввести единый `items_per_page = 9` в `create_keyboard_with_pagination` и во всех местах пагинации |
| 1.3 | Создать единую функцию `get_banned_users_data(page, items_per_page)` в `AdminService` или `helper_func`, возвращающую `(message_text, buttons_list)` для заданной страницы |
| 1.4 | Обновить `get_banned_users_list` и `get_banned_users_buttons` — использовать общий источник и пагинацию |
| 1.5 | Обновить `admin_handlers.get_banned_users` и `callback_handlers.change_page` — вызывать единую функцию с `page` |
| 1.6 | Прогнать тесты `test_keyboards_and_filters`, `test_callback_handlers`, `test_admin_handlers` |
**Затронутые файлы:**
- `database/repositories/blacklist_repository.py`
- `helper_bot/keyboards/keyboards.py`
- `helper_bot/utils/helper_func.py`
- `helper_bot/handlers/admin/services.py`
- `helper_bot/handlers/admin/admin_handlers.py`
- `helper_bot/handlers/callback/callback_handlers.py`
---
### Этап 2: Обогащение сообщений пользователей админам
| Шаг | Действие |
|-----|----------|
| 2.1 | Добавить в `AsyncBotDB`/репозитории методы: `get_posts_count_by_author`, `get_last_post_by_author`, `get_ban_history_count`, `get_last_ban_info`, `get_user_date_added` |
| 2.2 | Добавить в `BlacklistHistoryRepository` методы для истории банов (количество, последний бан) |
| 2.3 | В `UserService` или отдельном сервисе: метод `format_user_message_for_admins(user_id, message_text)` — собирает текст с данными пользователя |
| 2.4 | В `resend_message_in_group_for_message`: заменить `forward` на `send_message` с обогащённым текстом (имя, ник, id, посты, последний пост, баны, дата регистрации) |
| 2.5 | Сохранять `message_id` из результата `send_message` в `user_messages` (вместо `message.message_id + 1`) |
| 2.6 | Проверить, что `get_user_by_message_id` по-прежнему возвращает `user_id` для ответа админа |
| 2.7 | Обновить/добавить тесты для `resend_message_in_group_for_message` и `AdminReplyService` |
**Данные для обогащения:**
- Количество постов от пользователя
- Последний пост пользователя (текст)
- Количество банов
- Дата и причина последнего бана (если был)
- Дата создания пользователя в БД (первый контакт с ботом)
- Имя, ник, id пользователя (обязательно при send вместо forward)
**Шаблон итогового сообщения для админов:**
```text
👤 От: Иван Петров (@ivan_petrov) | ID: 123456789
📊 Постов в базе: 5
📝 Последний пост: "Привет, хочу поделиться мыслями о..."
📅 В боте с: 15.01.2025
🚫 Банов: 2
Последний: 20.02.2025, причина «Спам», истёк 27.02.2025
---
**Сообщение пользователя:**
**Почему удалили мой пост?**
```
**Правила форматирования:**
- Секции «Банов: 0» и «Последний: …» — показывать только если были баны
- «Последний пост» — обрезать до ~80 символов + «…» если длиннее; если постов нет — «Нет постов»
- Даты в формате `DD.MM.YYYY` или `DD.MM.YYYY HH:MM` для разбана
- Разделитель `---` перед текстом сообщения пользователя
**Для тестов:** использовать этот шаблон как эталон; проверять наличие всех секций, порядок полей, экранирование HTML в имени/нике/тексте.
**Затронутые файлы:**
- `database/async_db.py`
- `database/repositories/post_repository.py`
- `database/repositories/blacklist_history_repository.py`
- `database/repositories/user_repository.py`
- `helper_bot/handlers/private/private_handlers.py`
- `helper_bot/handlers/private/services.py`
- `helper_bot/handlers/group/services.py`
---
### Этап 3: Причина бана «Последний пост»
| Шаг | Действие |
|-----|----------|
| 3.1 | В `callback/services.py` в `ban_user_from_post`: заменить `message_for_user="Спам"` на `message_for_user="Последний пост"` |
| 3.2 | Обновить тесты, где ожидается «Спам» |
**Затронутые файлы:**
- `helper_bot/handlers/callback/services.py`
- `tests/test_callback_services.py` (если есть)
---
### Этап 4: Похожие посты (RAG + бот)
**Разделение работ:**
- **RAG сервис** — промпт в `.cursor/prompt-stage-4-similar-posts.md`
- **Подключение в боте** — ниже, раздел 4.2
#### 4.0. Текущая архитектура RAG сервиса
**Путь:** `/Users/andrejkatyhin/Work/PycharmProjects/rag-service`
| Компонент | Описание |
|-----------|----------|
| **VectorStore** (`app/storage/vector_store.py`) | In-memory хранилище векторов. `_positive_vectors` / `_negative_vectors` — для модерации. Персистентность: `positive_embeddings.npy`, `negative_embeddings.npy` или `vectors.npz`. Косинусное сходство через `np.dot` для нормализованных векторов. |
| **RAGService** (`app/services/rag_service.py`) | Модель `sentence-transformers/all-MiniLM-L12-v2` (384 dim). `get_embedding(text)`, `calculate_score(text)`, `add_positive_example`, `add_negative_example`. |
| **API** (`app/api/routes.py`) | `POST /api/v1/score`, `POST /api/v1/examples/positive`, `POST /api/v1/examples/negative`, `GET /api/v1/stats`, `POST /api/v1/warmup`, `GET/PUT /api/v1/scoring/params`. |
| **Config** (`app/config.py`) | `RAG_VECTORS_PATH`, `RAG_MAX_EXAMPLES`, `vector_dim=384`. |
**Важно:** Позитив/негатив — отдельные коллекции без `created_at`. Для похожих постов нужна **третья** коллекция с временными метками.
---
#### 4.1. Работы в RAG сервисе
##### 4.1.1. Расширить VectorStore
**Файл:** `app/storage/vector_store.py`
Добавить третью коллекцию для submitted-постов:
```python
# Новые атрибуты (аналогично _positive_vectors)
self._submitted_vectors: list = []
self._submitted_hashes: list = []
self._submitted_created_at: list = [] # Unix timestamps
self._submitted_post_ids: list = [] # post_id из бота
self._submitted_texts: list = [] # текст поста (для возврата в similar)
self._submitted_rag_scores: list = [] # rag_score на момент добавления
```
**Новые методы:**
| Метод | Описание |
|-------|----------|
| `add_submitted(vector, text_hash, created_at, post_id=None, text="", rag_score=None)` | Добавить пост в коллекцию submitted. FIFO при превышении `max_submitted` (новый лимит, например 5000). |
| `find_similar_submitted(vector, threshold, hours)` | Возвращает: `List[dict]` с полями `similarity`, `created_at`, `post_id`, `text`, `rag_score`. Фильтрует по `created_at >= now - hours*3600`. Сравнивает с `_submitted_vectors` через `np.dot`. Возвращает только те, где similarity >= threshold. |
**Персистентность:** Добавить в `save_to_disk` / `_load_from_disk` сохранение submitted-коллекции. Файл `submitted_embeddings.npz` с полями: `vectors`, `hashes`, `created_at`, `post_ids`, `texts`, `rag_scores`.
**Конфиг:** Добавить `RAG_MAX_SUBMITTED` (default 5000), `RAG_SUBMITTED_PATH` (путь к файлу submitted).
##### 4.1.2. Расширить RAGService
**Файл:** `app/services/rag_service.py`
| Метод | Описание |
|-------|----------|
| `add_submitted_post(text, post_id=None, rag_score=None)` | Очистить текст, получить embedding, `add_submitted` в vector_store. Сохраняет `text` и `rag_score` для возврата в similar. Вызывается при каждом suggest (бот передаёт rag_score из скоринга). |
| `find_similar_posts(text, threshold=0.9, hours=24)` | Получить embedding, вызвать `vector_store.find_similar_submitted`. Вернуть список похожих с полями: `similarity`, `created_at`, `post_id`, `text`, `rag_score`. |
##### 4.1.3. Добавить API endpoint
**Файл:** `app/api/routes.py`
**POST /api/v1/similar**
Возвращает: количество похожих постов, текст каждого, similarity (косинусное сходство), присвоенный rag_score на момент добавления.
```python
# Request
class SimilarRequest(BaseModel):
text: str = Field(..., min_length=1)
threshold: float = Field(default=0.9, ge=0.0, le=1.0)
hours: int = Field(default=24, ge=1, le=168) # 1ч7дней
# Response
class SimilarPostItem(BaseModel):
similarity: float # косинусное сходство (0.01.0)
created_at: int # Unix timestamp
post_id: Optional[int] = None
text: str # текст похожего поста
rag_score: Optional[float] = None # rag_score на момент добавления
class SimilarResponse(BaseModel):
similar_count: int
similar_posts: List[SimilarPostItem]
```
**POST /api/v1/submitted**
```python
# Request
class SubmittedRequest(BaseModel):
text: str = Field(..., min_length=1)
post_id: Optional[int] = None
rag_score: Optional[float] = None # для возврата в similar
# Response
class SubmittedResponse(BaseModel):
success: bool
message: str
submitted_count: int
```
**Примечание:** `POST /submitted` вызывается ботом при каждом suggest (после сохранения поста в БД). `POST /similar` вызывается ботом **перед** отправкой в группу модерации — чтобы проверить, есть ли похожие посты за последние сутки.
##### 4.1.4. Схемы и исключения
**Файл:** `app/schemas.py` — добавить `SimilarRequest`, `SimilarResponse`, `SimilarPostItem`, `SubmittedRequest`, `SubmittedResponse`.
**Файл:** `app/exceptions.py` — при необходимости добавить `SubmittedStoreError` (если коллекция пуста и т.п.).
##### 4.1.5. Автоочистка submitted (опционально)
В `autosave_loop` или отдельно: периодически удалять из `_submitted_*` записи старше N часов (например, 48), чтобы не раздувать память.
---
#### 4.2. Подключение в Telegram Helper Bot
> RAG сервис реализуется отдельно (промпт: `.cursor/prompt-stage-4-similar-posts.md`). Ниже — интеграция в бота.
##### 4.2.1. RagApiClient (`helper_bot/services/scoring/rag_client.py`)
Добавить методы:
- `find_similar_posts(text, threshold=0.9, hours=24)` — POST на `{api_url}/similar`, body `{"text": text, "threshold": threshold, "hours": hours}`. Вернуть `SimilarResponse` (или dataclass/dict) или `None` при ошибке.
- `add_submitted_post(text, post_id=None, rag_score=None)` — POST на `{api_url}/submitted`, body `{"text": text, "post_id": post_id, "rag_score": rag_score}`. При ошибке — логировать, не падать.
Оба метода проверяют `self._enabled` и не делают запросы, если RAG отключён.
##### 4.2.2. PostService (`helper_bot/handlers/private/services.py`)
В `_process_post_background` и `_process_media_group_background`:
**Порядок вызовов:**
1. Получить скоры (`_get_scores_with_error_handling`) — уже есть `rag_score`.
2. **Перед** отправкой: вызвать `find_similar_posts(original_raw_text, 0.9, 24)`. Если RAG недоступен или ошибка — не падать, пропустить.
3. Если `similar_count > 0`: добавить в `post_text` строку `\n\n⚠ Похожий пост за последние 24ч (совпадение {max_similarity:.0%})`.
4. Отправить пост в группу модерации.
5. Сохранить в БД.
6. **После** успешной отправки: вызвать `add_submitted_post(original_raw_text, sent_message.message_id, rag_score)` — в фоне. `rag_score` из шага 1.
**Важно:** Проверка similar — **до** добавления текущего поста в submitted.
##### 4.2.3. Доступ к RagApiClient
`RagApiClient` создаётся через `ScoringManager` или `BaseDependencyFactory`. PostService должен иметь доступ к `rag_client` (или `scoring_manager`). При необходимости добавить методы в `ScoringManager` как прокси к RAG.
##### 4.2.4. Обработка ошибок
При недоступности RAG — не падать, не добавлять предупреждение и не индексировать.
---
#### 4.3. Порядок вызовов в боте
```text
1. Пользователь отправляет пост
2. PostService._process_post_background:
a) Получить скоры (rag_score, confidence, ...)
b) find_similar_posts(text, 0.9, 24) — есть ли похожие? (возвращает count, text, similarity, rag_score)
c) Если да — добавить предупреждение в post_text
d) Отправить пост в группу модерации
e) Сохранить в БД (add_post)
f) add_submitted_post(text, message_id, rag_score) — индексировать в RAG
```
**Важно:** Проверка similar делается **до** добавления текущего поста в submitted, иначе пост будет похож сам на себя.
---
#### 4.4. Затронутые файлы
| Репозиторий | Файлы |
|-------------|-------|
| **rag-service** | `app/storage/vector_store.py`, `app/services/rag_service.py`, `app/api/routes.py`, `app/schemas.py`, `app/config.py`, `app/main.py` (описание в docs) |
| **telegram-helper-bot** | `helper_bot/services/scoring/rag_client.py`, `helper_bot/handlers/private/services.py` |
---
### Этап 5: Авто-публикация/отклонение (задел на будущее)
| Шаг | Действие |
|-----|----------|
| 5.1 | В `PostService._process_post_background`: после получения `rag_score` проверять пороги |
| 5.2 | Если `rag_score >= 0.8`: не показывать кнопки модерации, сразу публиковать (или вызывать логику publish) |
| 5.3 | Если `rag_score <= 0.4`: сразу отклонять (decline) |
| 5.4 | Добавить флаги в `.env` (например, `AUTO_PUBLISH_ENABLED`, `AUTO_DECLINE_ENABLED`) — по умолчанию `false` |
| 5.5 | Реализацию оформить как выключенную по умолчанию; включение — через конфиг |
**Затронутые файлы:**
- `helper_bot/handlers/private/services.py`
- `helper_bot/config/` или `.env`
---
### Этап 5.5: ML Scoring Статистика — восстановить полный вывод
**Проблема:** Раньше «📊 ML Статистика» показывала детали (модель, device, кол-во примеров, размерность). Теперь только API URL и статус.
**Причина:** Бот использует fallback (`get_stats_sync`) когда `RagApiClient.get_stats()` возвращает пустой результат. Это происходит при:
- 401/403 (ошибка авторизации)
- таймауте или ошибке соединения
- неверном формате ответа от API
**Задачи:**
| Шаг | Действие |
|-----|----------|
| 5.5.1 | Проверить, что RAG API `GET /stats` доступен с бота (сеть, CORS, API key). |
| 5.5.2 | Убедиться, что `RagApiClient.get_stats()` передаёт заголовок `X-API-Key` и корректно обрабатывает 200. |
| 5.5.3 | Проверить контракт ответа: RAG возвращает `model_name`, `model_loaded`, `device`, `vector_store` (positive_count, negative_count, total_count, vector_dim, max_examples). |
| 5.5.4 | При ошибке API — логировать причину (status, body) и при необходимости улучшить fallback-сообщение (например, «API недоступен: …»). |
| 5.5.5 | Добавить тесты для `get_ml_stats` с моком API (успешный ответ и fallback). |
**Затронутые файлы:**
- `helper_bot/handlers/admin/admin_handlers.py` (get_ml_stats)
- `helper_bot/services/scoring/rag_client.py` (get_stats, get_stats_sync)
- `helper_bot/services/scoring/scoring_manager.py` (get_stats)
---
### Этап 6: Тесты и качество кода
| Шаг | Действие |
|-----|----------|
| 6.1 | Прогнать все тесты: `pytest tests/ -v` |
| 6.2 | `make code-quality` (или `isort`, `black`, `flake8`) |
| 6.3 | При необходимости обновить моки и фикстуры |
---
### Этап 7: Release Notes и деплой
| Шаг | Действие |
|-----|----------|
| 7.1 | Создать `docs/RELEASE_NOTES_DEV-N.md` по шаблону из `.cursor/rules/release-notes-template.md` |
| 7.2 | Коммиты в формате: `feat:`, `fix:`, `refactor:` |
| 7.3 | Push в `dev-N` → CI (тесты, code quality) |
| 7.4 | Создать/обновить PR в `main` |
| 7.5 | После мержа — деплой по `deploy.yml` (если настроен в prod) |
---
## Зависимости между этапами
```
Этап 0 → Этап 1, 2, 3 (можно параллельно)
Этап 1, 2, 3 → Этап 6
Этап 4 зависит от RAG (отдельный сервис)
Этап 5 можно делать после 4 или независимо
Этап 5.5 — независимо (можно параллельно с 13)
```
---
## Рекомендуемый порядок реализации
1. Этап 0 — подготовка
2. Этапы 1, 2, 3 — независимо, можно в любом порядке
3. Этап 4 — после готовности RAG
4. Этап 5 — после 4 или параллельно с 13
5. Этап 5.5 — разобраться с ML Scoring Статистикой (можно параллельно)
6. Этап 6 — перед PR
7. Этап 7 — после ревью и мержа
---
## Ссылки на документацию проекта
- `.cursor/rules/my-custom-rule.mdc` — общие правила
- `.cursor/rules/architecture.md` — архитектура
- `.cursor/rules/handlers-patterns.md` — паттерны handlers
- `.cursor/rules/release-notes-template.md` — шаблон Release Notes
- `prod/.cursor/rules/my-custom-rule.mdc` — CI/CD, ветки, деплой