АРХИТЕКТУРА ПРОЕКТА — КОНСУЛЬТАЦИИ И ЗАПИСЬ

Исходная схема с точечными дополнениями. Один салон · до 10 мастеров · около 200 пишущих клиентов в день.

Структура и основные решения сохранены. Синий пунктир — добавленный шаг; новые требования ещё нужно реализовать.

К веткам записи и консультации ↓ · К конфигурации сервера ↓

процесс решение заглушка / выход хранилище канал / отправка вызов облачной LLM локальная ML-модель (без облака) ещё не реализовано

🗺 Поток одним взглядом

1 · Каналы — четыре адаптера

✈️ Telegram — контейнер llm
aiogram long-polling кнопки — родные inline · голос скачивается файлом
исходящие через OUTBOUND_PROXY_URL (SOCKS5), пусто → напрямую
📱 WhatsApp — контейнер wappi
Wappi webhook :8081 /webhooks/wappi/callback через Caddy · заголовок Authorization = секрет (fail-closed)
только личные чаты (@g.us — отказ) · голос ptt/audio приходит base64 в теле
кнопки → нумерованное текстовое меню (карта в Redis, TTL 1 ч, атомарное поглощение ответа-цифры)
amoCRM — штатный виджет Wappi (контакты/сделки без нашего API-кода)
📸 Instagram — контейнер instagram
Meta Instagram Login API :8082 GET-верификация hub.challenge + POST c HMAC X-Hub-Signature-256 · фильтр эха своих ответов
вход в РФ; зарубежный релей исходного тела требует отдельной правовой проверки
если без него канал не работает — запуск канала до решения отложить
исходящие в graph.instagram.com — через IG_PROXY_URL (РКН режет SNI) + форс IPv4
лимит 1000 симв. → авторазбиение · username добирается Graph API · голос — вложение по URL
/internal/send — ответ администратора (токен, только docker-сеть)
🔵 ВКонтакте — контейнер vk
VK Callback API :8083 group_id + secret (fail-closed) · confirmation-код · только личные message_new
исходящие messages.send (random_id) · screen_name с кешем
голос audio_message по URL (ogg/mp3) · кнопки → то же нумерованное меню
/internal/send — ответ администратора

Идентификация без миграций: WhatsApp / Instagram / VK живут в зарезервированных отрицательных BIGINT-диапазонах chat_id (−9·10¹⁸+телефон · −8·10¹⁸+IGSID · −7·10¹⁸+user_id), Telegram — как есть. channels.channel_for_chat() восстанавливает канал из chat_id в любом месте кода.

ОБЩИЙ ГЕЙТ БЫСТРЫХ ОТКАЗОВ — одинаков во всех 4 адаптерах, модель не тратим
Бот на паузе?
тумблер bot:paused из админки
да
«Временно недоступен»
нет
Согласие на ПДн есть?
проверка в Postgres
нет
Запросить согласие ссылка на условия + «Согласен»; реклама отдельно
сохранить выбор, дату и версию; попросить повторить запрос
до согласия не сохранять тело и не скачивать голос
да
Дубль?
ID события в БД
Redis 1 ч — только кэш
да
Дроп (тихо)критично для вебхуков — Meta/VK ретраят
нет
Rate-limit?
20 сообщ. / скользящие 60 с на chat_id
превышен
«Слишком часто»предупреждение только на первом отказе
ок
Тип содержимого?
🖼 фото / видео / стикер / файл → «Работаю только с текстом»
🎙 голос → лимиты 120 с / 4 МБ, формат из белого списка → единое voice-событие
(не влез в лимит → «Голосовое до 2 минут», не скачалось → «Напишите текстом»)
текст / голос
Длина > 4000?
да
«Слишком длинно»
нет
Иностранный язык?
нет кириллицы и ≥10 латинских букв
да
«Давай по-русски»
валидно
Сохранить событие → RabbitMQ нормализовать событие и сохранить его с заданием публикации в Postgres
приём подтверждать после commit; сбой RabbitMQ → повторить публикацию из БД
нет commit → отказ / повтор доставки канала; trace_id сохраняется
RabbitMQ 4 — буфер между адаптерами и мозгом shard.q.{crc32(chat_id) % 16} — 16 durable-очередей · сообщения persistent, переживают рестарт
DLX → dlq (сообщения-убийцы) · prefetch = 8 на шард (WORKER_SHARD_PREFETCH)
один чат всегда в одном шарде · порядок склейки серий держит буфер воркера
WORKER_SHARDS="0-7" — задел на несколько воркеров

3 · Воркер — диспетчер событий и буфер склейки

КОНТЕЙНЕР worker · консьюмеры 16 шардов + весь пайплайн (2 CPU / 3 ГБ)
Получено событиепроверить, не отозвано ли согласие; при отзыве остановить обработку
journal: worker.received · битый JSON → DLQ
Уже обработано?
статус события / операции в БД
Redis 24 ч — кэш
да
ack без повтораидемпотентность
нет
Чат на ручном
управлении?
обращение в БД; Redis — кэш
да
Бот молчит, админ ведёт сообщения сохраняются в историю (при согласии)
ручной режим до явного закрытия обращения
нет
Ответ на реактивацию? любое входящее гасит серию игноров всего телефонного профиля
(best-effort, сбой не блокирует ответ) — дальше обычный путь
Тип события?
кнопка
Callback-роутер consent: · bookflow: · resched: · remind: · rate:
оценка: IDOR-защита — чужая бронь тихо игнорируется
message / voice / command
🎙 Голос → текст: faster-whisper tiny локально в воркере, CPU INT8, русский, VAD, один файл за раз (lock)
голос распознаётся локально; STT сам по себе не означает биометрию
распознанные параметры повторить клиенту перед записью
аудио хранится в messages BYTEA (РФ-контур), админка даёт плеер
сбой STT → «напишите текстом»
+ алерт voice_stt_failed
Буфер склейки — режим auto спокойный чат → без искусственной задержки
серия (пауза < 3 с) → rapid-флаг на 60 с → склейка: базовая задержка 3.5 с (3 с если бот только что отвечал),
+1 с за сообщение, потолок 6 с / 5 с · recovery буферов после рестарта
3 неудачных флаша → пауза 60 с + алерт; незавершённое задание остаётся в БД
состав склейки и результаты сохраняются: исчезновение Redis не повторяет действие
флаш
Флаш уже отвечен?
сохранённый результат задания
Redis 10 мин — кэш
да
Пропускrecovery-повтор
нет
Перехват без LLM?
⏳ ответ по текущему шагу → в цепочку записи/переноса
↩ «назад» / «прекратить» / «человек» работают на любом шаге
💬 другой вопрос → сохранить черновик, ответить, предложить продолжить
💬 pending-комментарий к низкой оценке → сохранить + «передали руководителю»
✅ pending «придёте?» → «да/приду» = подтверждение, «нет/отмен» = цепочка переноса
нет — в security-цепочку ↓

4 · Единая цепочка защиты входа

🔒 ЗАЩИТА ВХОДА — одна на весь пайплайн (worker)
Guardrails входа?
regex, без LLM
невалид
Вежливый отказ-заглушкамодель не тратим
7 категорий: role_switch · prompt_leak · authority ·
system_tags · policy_patch · separator · known_attack
NFC-нормализация + вычистка zero-width
валидно → sanitize (теги, разделители)
Контекст диалогаRedis 40 последних (TTL = retention ПД) · нет → Postgres
🔒 Маскировка PII — 3 слоя, всё локально 1) regex + контрольные суммы: паспорт, СНИЛС, ИНН 10/12, карта (Луна), полис ОМС,
телефон РФ, email, @username, IP, дата рождения, свидетельство о рождении
2b) pymorphy3 — падежные имена/организации (падежные формы)
2) Presidio + spaCy ru_core_news_lg — PERSON / LOCATION / ORGANIZATION
единый mapping на передаваемый контекст · минимум данных перед облаком
NER может пропустить ПД: маскирование не гарантирует полного обезличивания
journal: worker.pii_masked — счётчики и технические метрики
полный текст и карта замен по умолчанию не журналируются
NER упал
fail-closed на старте: воркер не поднимается
в рантайме: сообщение НЕ уходит, буфер копит
алерт CRITICAL · вызов в облако заблокирован
🛡 Input Security LLM ∥ Роутер — параллельно два независимых вызова дешёвой LLM стартуют одновременно
security: вердикт OK/BLOCK · каскад nano → Haiku → fail-open + алерт (regex-слой остаётся)
повторные OK — из Redis-кэша sec:ok: (TTL 1 ч, байт-в-байт; BLOCK и fail-open не кэшируются)
вердикт + источник (cache / llm / fail-open) пишутся в журнал; fail-open — warning-след
вердикт security первичен: BLOCK → роутер-таска отпускается доехать в фоне (usage учитывается), вердикт выбрасывается
BLOCK
Вежливый отказjailbreak · выпытывание промпта
чужие ПД · вред · токсичность · обфускация
OK
Роутер — одно или несколько намерений Structured Outputs: список маршрутов, параметры и зависимости
обычный код проверяет допустимые действия, противоречия и циклы
видит 6 последних сообщений контекста · безопасностью НЕ занимается
эскалация — явный маршрут, а не порог уверенности (короткое «нет» не зовёт оператора)
сбой → fail-open в consultation + алерт router_down
consultation · booking · my_bookings · reschedule ·
review · smalltalk · offtopic · escalation · other

5 · Ветвление по маршруту

Запустить нужные веткиодна задача → одна ветка; независимые задачи → до 3 параллельно
зависимые — по очереди; изменения — после подтверждения
человек → передать диалог и остановить автоматический сценарий
📅 booking — запись
0. Услуги и посетителиодна / несколько услуг, один / несколько человек; одновременно или подряд
уточнить по каталогу → рассчитать длительность каждого визита
1. Подходящий мастер — кнопкитолько выполняющие выбранные услуги + «любой подходящий»
черновик в БД; Redis TTL 30 мин — кэш, истечение не создаёт запись
2. Слоты — кнопкипо 8 вариантов + «следующие» / «другая дата» во всём горизонте 90 дней
минимум за 2 ч · буфер 20 мин между визитами · график из настроек
для группы подобрать совместимые интервалы для всех посетителей
3. Имя + телефон текстомпарсинг из свободного текста · нормализация +7XXXXXXXXXX
телефон = якорь профиля между каналами
Подтвердить итог?услуги, люди, мастера, дата/время, стоимость и контакт
нет → изменить / закрыть черновик; да → сохранить ↓
4. Бронь → Postgres в одной транзакции: проверить график/пересечения/лимит, сохранить всю группу и результат
EXCLUDE по занятым интервалам мастера; лимит 4 — только будущие подтверждённые визиты
слот занят → откат и другие варианты; уникальный ID операции защищает от повтора
5. Предоплата ЮKassa ссылка → webhook с подписью · авто-возврат при отмене Ф7 · ДЕНЬГИ
6. Апсейлдобавочная услуга → пересчитать время/цену → снова подтвердить изменение
Результат записиномер и итог из БД → в общий ответ; текст детерминированный
tool-use для брони не используем
краш → фолбэк + алерт; продолжить сохранённый шаг без повторной записи
📋 my_bookings — мои записи
Найти свои записибудущие визиты профиля; история — по запросу
ничего нет → сообщить и предложить запись
Показать карточкипосетитель, услуги, мастер, дата/время, стоимость, статус и номер
Просмотр или изменение?только посмотреть → общий ответ
перенести / отменить → соседняя ветка
🔄 reschedule — перенос / отмена
Свои будущие бронивыбрать визит или группу этого профиля
черновик в БД; Redis TTL 30 мин — кэш
Политика< 1 ч до визита → только звонок админу
лимит 3 переноса на бронь
Если перенос / изменениедругое время, мастер, услуга или посетитель → новый расчёт → подтверждение
атомарно проверить и заменить; не удалось → старая запись остаётся
пересчитать напоминания
Если отменавыбрать визит или группу → подтвердить → отменить и освободить интервалы
Результатсохранённый итог → в общий ответ
💬 consultation · review · smalltalk · other
Компакт контекстадиалог > 30 сообщений → старое в сводку (nano) + 10 последних
сбой сжатия → просто хвост
Основная LLM системный промпт system.md (прайс статикой) + canary-токен в конце
каскад: primary с ретраями (backoff, классификация ошибок retryable/quota/fatal)
→ fallback claude-haiku-4-5 → все легли: «Перегружен» + алерт llm_down
hot-reload промпта по Redis pub/sub — без рестарта
→ раздел 6 · Защита выхода
🙅 offtopic
«Здесь только про студию»шаблон, LLM не тратим
ответ сохраняется в контекст
🚨 escalation — явный маршрут
Триггерыпрямая просьба человека · контакт руководителя ·
жалоба / претензия / возврат · юрвопрос / форс-мажор
Бот замолкаетсначала сохранить обращение в БД
ручной режим до явного закрытия
алерт WARNING админу
Ответ клиентупосле сохранения: «передал администратору»
ночью — ближайшее рабочее время; обращение не пропадает, утром уведомить сотрудника
Админ ведёт диалогиз /escalations или карточки чата · «Вернуть бота» →
одноразовый resume-предохранитель (первый неясный вопрос
не эскалирует повторно, бот просит уточнить)
Объединить результаты → один ответдождаться веток или их таймаута; сохранить результаты и зависимости в БД
проверить LLM-часть по разделу 6; итог записи брать из операции
готовое + ошибки + максимум один следующий вопрос → общая доставка

независимые ветки сохраняют свои результаты; ошибка одной не стирает другую
один визит и групповая запись изменяются атомарно; зависимые изменения не запускать одновременно

6 · Защита выхода и отправка (путь основной LLM)

🔓 ЗАЩИТА ВЫХОДА
Canary в ответе?
проверяется всегда
утёк промпт
Ответ зарезан+ regex-паттерны утечки
(«системный промпт», [INST], имена модулей)
чисто
Валидатор ответа — nano язык / обрыв / артефакты / грубость → ровно одна регенерация (итог — в журнал)
видит маски, не реальные ПД · сбой валидатора → пропуск
тумблер на /bot-control (Redis validator:enabled, действует сразу) · нет ключа → env-дефолт
ТУМБЛЕР АДМИНКИ · дефолт OUTPUT_VALIDATOR_ENABLED=false
+1 вызов nano на каждый ответ
при включении флага
🔓 Демаскировка PII маски → реальные телефоны/имена · выдуманные LLM плейсхолдеры вычищаются
оригиналы остаются в разрешённых локальных хранилищах и готовом ответе
Guardrails ответа?
невалид
Безопасная заглушка
валидно
Сохранённый ответ → ChannelBot Telegram напрямую (retry на flood-лимит) · WhatsApp/IG/VK — свои клиенты,
кнопки → нумерованные меню · Markdown-мусор (**жирный**) стрипается
Учёт результатаистория / задание ответа — до отправки; после неё — статус доставки
контекст → Redis · usage токенов → Postgres
(учитываются ВСЕ вызовы: security, роутер, компакт, валидатор, primary, fallback, регенерации)

7 · Фоновые процессы

КОНТЕЙНЕР scheduler · цикл 60 сек · исходящие с троттлингом 0.1 с
Напоминания — лестница T-24 / T-3 / T-1 T-24: «придёте?» с кнопками [Приду | Перенести] + pending-флаг (ответ текстом тоже ловится)
T-3: повтор ТОЛЬКО при игноре · T-1: информация без вопроса
время всегда в МСК · «заблокировал бота» → пропуск без ретрая · блип Redis не глушит ступень
выборки пока Telegram-only (для WA/IG вне 24-ч окна нужен SMS — не построен)
Отметка «пришёл»
+ 30 мин после услуги?
ставит админ в дашборде
нет отметки
Оценку не просим
да
Запрос оценки ⭐1–5кнопки
≤ 3
Алерт + офферскидка 30% / бонус-уход · просьба рассказать —
комментарий ловится pending-веткой воркера
📣 Реактивация авто раз в московские сутки: follow-up (30 дн. после визита) + спящие (45 дн.) —
авто-режим включается owner'ом из админки (по умолчанию выключен)
ручные кампании из админки: сегмент all/followup/dormant/regular + свой оффер
жёсткие гейты: телефон + рекламное согласие · лимит 5/мес · стоп после 3 игноров ·
WhatsApp/IG только в 24-ч окне переписки · резерв доставки до send; неопределённая доставка требует сверки
текст пишет nano по офферу (guardrails + шаблонный fallback) · LLM не решает, кому слать
постоянникам (3+ визита) — скидка из настроек · ответ клиента гасит игноры и идёт обычным путём

🔁 Фоны воркера

  • Чистка retention — раз в сутки: диалоги 3 года, журнал 90 дней
  • Монитор каждые 2 мин: диск < 10% → алерт; возраст очереди >30 с — предупреждение, >120 с или >500 заданий — ограничить новый приём
  • Alive-маркер per-slot: нештатный рестарт (OOM/kill) детектится на старте → алерт
  • Prompt reload — Redis pub/sub, промпт перечитывается без рестарта
  • HTTP воркера :8082 (docker-сеть): /pii/mask для админ-эвала «Стирание ПД»

🚨 Алерт-бот (отдельный TG-бот)

  • Дедуп по типу 5 мин + глобальный колпак 20/мин — лимитер in-memory (алерт про упавший Redis не зависит от Redis)
  • Уровни CRITICAL 🔴 / WARNING 🟡 · никогда не роняет пайплайн
  • Типы: dlq, infra, llm_down, llm_quota/auth, security_down, router_down, pii_ner_*, escalation, low_rating, chain_error, flush_retry, voice_stt_failed, worker_restart, disk_low, queue_growing, llm_stub_active…

⚠️ Обработка сбоев (worker)

  • Инфра легла (PG/Redis) → nack без счётчика + backoff до 30 с, ждём восстановления
  • Прочая ошибка → 3 ретрая с backoff → DLQ + алерт
  • Краш → фолбэк + алерт; ACK после результата или надёжной передачи в задание БД, завершённые операции не повторять
  • Мёртвый консьюмер шарда → exit(1) → docker перезапускает, unacked возвращаются
  • Оба LLM легли → ограниченные повторы сохранённого задания; затем явная ошибка / обращение человеку
  • NER упал → fail-closed, очередь копится (152-ФЗ)
  • SIGTERM → дорабатываем in-flight до 15–30 с → graceful выход
  • Канал принял ответ, но статус не сохранён → сверка / разбор; без поддержки канала отсутствие дублей ответа не гарантируется

💾 postgres-backup

  • Раз в сутки pg_dump | gzip → ./backups, ротация 7 дней (после успешного дампа)
  • Внешний слой — бэкапы провайдера · restore-процедура: gunzip -c … | psql (runbook)

8 · Админка (FastAPI + Jinja, контейнер admin)

СтраницаРольЧто умеет
/ — диалогиownerСписок чатов по каналам (@user_tg / +7…_what / @user_inst / id_vk), токены и стоимость по каждому чату
/chats/{id} — карточкаowner/staffПрямой чат: ответить от имени бота, атомарно забрать на ручное управление, «вернуть бота» (resume-предохранитель); автообновление; плеер голосовых (/messages/{id}/media); удаление клиента целиком (152-ФЗ ст. 21, все каналы + Redis-ключи, брони обезличиваются) — owner
/statsownerГлобальные токены/стоимость/кэш-экономия · автоответы → сэкономленные часы и рубли оператора · последние инциденты
/bookingsowner/staff/masterЗаписи + календарь, walk-in вручную, отметка «пришёл», отмена
/mastersmaster — только еёГрафики по дням недели, отпуска/отгулы, «уведомить затронутых» при недоступности
/escalationsowner/staffАктивные эскалации, ответ клиенту в исходный канал (TG/WA/IG/VK), снятие эскалации; обновление раз в 5 с
/servicesownerПрайс: услуги/цены/длительности → публикация в системный промпт
/promptownerРедактор system.md, версии + откат, публикация → hot-reload воркера
/reactivationownerНастройки авто-режима (пороги 30/45 дн., лимиты, скидка постоянника, оффер, LLM-инструкция) + ручные кампании по сегментам + журнал доставок sent/skipped/failed
/eval — 6 вкладокownerСм. раздел 9
/journal · /logs · /bot-controlownerЖурнал по trace_id — полный след пайплайна (счётчики маскирования, security-вердикт с источником, все обрывы: пауза/дедуп/отказы голоса/IDOR) · хвосты логов контейнеров · тумблеры паузы бота и валидатора ответов

🔐 Вход и RBAC

9 · Эвалы — 6 вкладок в админке + 2 CLI-набора

СьютКейсовЧто проверяетКак судится
Основная LLM100Ответы бота на прод-промпте (цены, запись, границы, безопасность); галочка «с валидатором LLM»2 ступени: regex/keywords → LLM-судья gpt-5-mini, порог 0.8
Роутер100одиночные и смешанные намерения, параметры, зависимости, escalationправильный набор маршрутов и следующий шаг
Валидатор100OK/BAD на готовых ответах (обрывы, не-русский, артефакты)вердикт nano vs эталон
Input Security5030 block / 20 ok, ловушки false-positive, PII-маски в текстевердикт security-LLM vs эталон
Стирание ПД100Боевой pii.mask_pii через worker /pii/mask: падежи, ошибки, контакты, история и STT; измерять пропуски по типам ПД и ложные срабатываниядетерминированно, без облака
CLI: adversarial · NER20 · 15run_evals.py / ner_eval.py — атаки и качество NER, гоняются руками

🧪 Механика

🧬 Юнит-тесты

10 · Данные

🐘 Postgres 16 — основные данные

🟥 Redis 7 (AOF) — семейства ключей

🐇 RabbitMQ 4

11 · Инфраструктура — Docker Compose, 13 сервисов

СервисОбраз / командаCPU / RAMHealthcheckРоль
llm./llm target runtime · bot.py0.5 / 384 МБсвежесть bot.log (пульс 60 с)Telegram-адаптер (polling)
worker./llm target worker (+PyTorch CPU) · worker.py2 / 3072 МБсвежесть worker.logВесь пайплайн; NER ~1 ГБ и Whisper в этом же контейнере
wappi./llm runtime · wappi_adapter.py0.3 / 256 МБGET /health :8081WhatsApp webhook
instagram./llm runtime · instagram_adapter.py0.3 / 256 МБGET /health :8082Instagram webhook
vk./llm runtime · vk_adapter.py0.3 / 256 МБGET /health :8083VK Callback
scheduler./llm runtime · scheduler.py0.3 / 256 МБсвежесть scheduler.logНапоминания, оценки, реактивация
admin./admin · FastAPI :80800.5 / 384 МБGET /loginДашборд; порт наружу только 127.0.0.1
migrate./llm runtime · alembic upgrade head0.5 / 512 МБразовый (completed)Миграции до старта обработчиков
postgrespostgres:16-alpine1 / 1024 МБpg_isreadyОсновное хранилище (РФ-контур)
redisredis:7-alpine · AOF + requirepass0.5 / 256 МБPINGКэш/состояния
rabbitmqrabbitmq:4-alpine0.5 / 512 МБdiagnostics pingОчередь
caddycaddy:2-alpine · порты 80/4430.5 / 256 МБadmin API :2019Авто-HTTPS (Let's Encrypt), см. ниже
postgres-backuppostgres:16-alpine · backup.sh0.5 / 256 МБ—Суточный дамп + ротация 7 дн.

⚙️ Конфигурация сервера — стартовый ориентир

🌐 Caddy — единственный вход снаружи

📦 Сборка и деплой

🕳 Прокси и обходы РКН/гео

12 · Модели — кто за что отвечает

РольМодельГдеПримечания
Основная (консультация)gpt-5.4-nanoоблакоLLM_REASONING_EFFORT=none · модель задаётся LLM_MODEL
Fallback основнойclaude-haiku-4-5облако (Anthropic native)отдельный ключ · подхватывает при quota/fatal/ретраях primary
Дешёвая: security · роутер · компакт · валидатор · реактивацияgpt-5.4-nanoоблакоreasoning не передаётся (дефолт none) · Structured Outputs для роутера
Судья эваловgpt-5-miniоблакопорог 0.8 · температура опускается для reasoning-моделей
Голос → текстfaster-whisper tiny INT8локально, CPU воркерааудио распознаётся локально · 120 с / 4 МБ
PII NERspaCy ru_core_news_lg + Presidio + pymorphy3локальноfail-closed · ~1 ГБ RAM

Код модель-агностик: LLM_BASE_URL позволяет OpenAI / Anthropic / OpenRouter / DeepSeek / Ollama. LLM_STUB/TG_STUB — нагрузочные заглушки (включение primary-заглушки алертится).

13 · Безопасность и 152-ФЗ — принципы

🛡 152-ФЗ и данные

🔒 Защита от атак и доступ

Архитектура чат-бота для консультаций и записи. Исходная структура сохранена; дополнения описывают требования к реализации.