# teeu — AI-слой и нейромодерация (ТЗ §12–13)

Единый слой поверх DeepSeek. Ни для одной сущности **нет** отдельного HTTP-клиента — всё через
`AiProvider`. Модель/ключ — из настроек (БД, шифрованно) с fallback в `config/services.php`;
модель нигде не хардкодится.

## Компоненты

| Слой | Файлы |
|---|---|
| Провайдер | `Services\Ai\Contracts\AiProvider` → `DeepSeekAiProvider` (`chatJson`) |
| Результат | `Services\Ai\DTO\AiResult` (ok/data/errorCode/tokens/latency/attempts) |
| Лог | `ai_requests` + модель `AiRequest` (provider, model, purpose, target, prompt_version, request_hash, status, decision, response_json, error_code, attempts, latency, tokens) |
| Модерация | `Services\Ai\Moderation\AbstractModerationService` + `ModerationResult` + `SellerDescriptionModerationService` |
| Алерты | `Services\Telegram\TelegramService` + `Jobs\SendTelegramAlert` |

## DeepSeekAiProvider

- `chat/completions` с `response_format: json_object`, `temperature: 0`.
- **Retry** с экспоненциальным backoff + jitter только для transient-ошибок (`rate_limit`, `timeout`,
  `http_5xx`, `overloaded`, `invalid_json`); `insufficient_balance` / `http_4xx` не ретраятся.
- Классификация ошибок → `errorCode`; каждый вызов пишется в `ai_requests` (успех/неуспех).
- **Structured JSON**: ответ парсится и (в доменных сервисах) валидируется по схеме + семантике —
  недостаточно `json_decode() !== null`.
- **Telegram-алерт** при системных ошибках (`insufficient_balance`/4xx/5xx/timeout/overload) с
  дедупликацией (≤1 на группу / 30 мин, ТЗ §12.6). Ключ/токен/полные payload не логируются.

## Политика недоступности AI (ТЗ §12.5) — fail-closed для нового контента

`ModerationResult::unavailable()` возвращается, если AI выключен, вернул ошибку или невалидный JSON.
Тогда контент **остаётся pending** и не публикуется; уже одобренное не скрывается. Модерация никогда
не «угадывает» allow/block при невалидном ответе.

| Сущность | AI недоступен |
|---|---|
| Новый product text | pending, не публикуется |
| Новая seller description | новая версия pending; **старая approved показывается** |
| Новый review / chat message | pending, не виден |
| Новая AI-категория | pending, товар без категории не публикуется |

## Versioned policies

Каждая политика версионируется (`ai_requests.prompt_version`): сейчас `seller_description_v1`.
Далее — `product_text_v1`, `chat_v1`, `review_v1`, а также классификация категорий.

## Модерация описания продавца (ТЗ §7.1, §13.2)

`SellerDescriptionService::submit` кладёт текст в `description_pending` (status pending) и ставит
`ModerateSellerDescriptionJob` (очередь `ai`). Джоб: allow → переносит в `description` (approved),
чистит pending; block → status `rejected` (публично остаётся прошлая одобренная версия); review →
`needs_review`; недоступность → остаётся pending (ретрай планировщиком, Этап 10).

## Ограничение

Текстовая модерация не гарантирует обнаружение нарушений, видимых только на фото (ТЗ §68). Image
moderation — отдельная будущая интеграция.

## Выключатель модерации товаров

Настройка `ai.moderation.products` (админка → **Настройки → Интеграции → Модерация товаров**).
По умолчанию включена.

**Выключение и сбой AI — разные вещи, и путать их нельзя.** Fail-closed остаётся: если проверка
включена, а провайдер молчит, товар ждёт и на витрину не выходит. Выключатель означает другое —
владелец решил не проверять вовсе, и тогда товар публикуется сразу. На оба случая есть тесты, и
тест про недоступный AI написан именно ради того, чтобы выключатель однажды не «починили» так, что
сбой начнёт публиковать.

**Постановка на модерацию идёт через `ModerateProductTextJob::enqueue()`, а не `dispatch()`.**
При выключенной проверке задание не создаётся вовсе: товар одобряется на месте. Иначе выключение
не давало бы ничего, кроме тысяч пустых заданий в очереди `ai`. Проверка продублирована в `handle()`
— задания, поставленные до выключения, уже лежат в очереди и разбираются без обращения к AI, за
минуты вместо часов.

Метод называется `enqueue`, а не `queue`: `queue()` — это хук Laravel на самой задаче, и диспетчер
вызвал бы наш метод вместо своего.

**Категория важнее текста.** Товар без определённой категории не публикуется и при выключенной
модерации: одобряется только текст, на витрину он выйдет после классификации.

**Область — только товары.** Отзывы, сообщения чата и описания магазинов проверяются по-прежнему:
там пользовательский текст, и риск другого рода.
