README.md

Stomatology Booking Bot

Production-ready Telegram-бот записи в стоматологию. Config-driven шаблон, готовый к развёртыванию под нового клиента за 1-2 часа.

CI Python aiogram FastAPI PostgreSQL Tests License


Зачем это нужно

Малые и средние стоматологии в РФ теряют 20-30% входящих заявок: запись только по телефону, после 19:00 никто не отвечает, неявки на приёмы 15-22% от загрузки. Найм администратора закрывает часть проблемы за 50-80 тыс. ₽/мес. Этот бот закрывает её за 9 900 ₽/мес и работает 24/7.

Целевая аудитория: 1-3 врача, 30-100 записей в неделю, бюджет на digital до 200 тыс. ₽/год.

Что бот умеет

Для пациентов

  • 📋 156-ФЗ согласие отдельным документом (ФЗ №156 от 24.06.2025) — двухкнопочный flow с PDF + audit log с SHA-256 хэшем документа
  • 📅 Запись на приём: услуга → врач → дата → время → подтверждение
  • 🔒 Защита от двойной записи через pg_advisory_xact_lock + SELECT FOR UPDATE — 100 одновременных кликов на один слот = ровно одна успешная запись
  • Напоминания за 24 часа и за 1 час до приёма (Telegram, опционально SMTP)
  • ↩️ Отмена и перенос записи без звонка администратору
  • 🤖 AI-роутер (опционально): GigaChat классифицирует намерение пациента (book / cancel / reschedule / services / doctors / faq / smalltalk / junk) и направляет в нужный handler
  • AI-FAQ (опционально): GigaChat отвечает на вопросы о клинике с многослойными guardrails (см. Безопасность AI)

Для администратора (отдельный admin-bot)

  • 📊 Просмотр записей за сегодня / завтра / неделю
  • ✏️ Управление услугами, врачами, расписанием
  • 🚫 Отмена / перенос от лица клиники
  • 📈 Отчёты по периодам
  • 🔐 Whitelist по ADMIN_TELEGRAM_IDS

Технологический стек

Слой Технология Версия
Runtime Python 3.13
Bot framework aiogram 3.27
Web framework FastAPI 0.136
ORM SQLAlchemy 2.0 (async) 2.0.49
Migrations Alembic 1.18
DB PostgreSQL 17
FSM / Cache Redis 7.4
AI (опц.) GigaChat 2 Lite/Pro SDK 0.2
Logging structlog 25.5
Testing pytest + testcontainers
Linting ruff + mypy
Container Docker Compose

Архитектура

                           ┌─────────────────┐
                           │   Pacient(s)    │
                           │  in Telegram    │
                           └────────┬────────┘
                                    │
         ┌──────────────────────────┴──────────────────────────┐
         │                                                      │
         ▼                                                      ▼
┌─────────────────┐                                   ┌─────────────────┐
│  patient-bot    │                                   │   admin-bot     │
│   (aiogram)     │                                   │   (aiogram)     │
└────────┬────────┘                                   └────────┬────────┘
         │                                                      │
         │  consent flow → booking → reminders                 │  CRUD operations
         │  + AI router + AI FAQ                               │  reports + manage
         │                                                      │
         ▼                                                      ▼
┌──────────────────────────────────────────────────────────────────────┐
│                      Domain Layer (booking_bot.domain)               │
│  booking | cancel | reschedule | consent | slot_generator           │
│  ai_router | ai_faq | ai_guardrails | ai_circuit_breaker            │
│  ai_client | rate_limiter | prompt_loader                            │
└────────────────────────┬─────────────────────────────────────────────┘
                         │
              ┌──────────┼──────────┬──────────────┐
              ▼          ▼          ▼              ▼
       ┌──────────┐ ┌─────────┐ ┌────────┐ ┌──────────────┐
       │PostgreSQL│ │  Redis  │ │GigaChat│ │ FastAPI app  │
       │   (17)   │ │  (7.4)  │ │  API   │ │  /healthz    │
       │          │ │  + FSM  │ │        │ │  /readyz     │
       │ 10 tables│ │  + RL   │ │        │ │              │
       └──────────┘ └─────────┘ └────────┘ └──────────────┘
              ▲
              │ poll every 60s
              │ FOR UPDATE SKIP LOCKED
              │
       ┌──────┴───────────┐
       │ reminder worker  │
       │ (separate proc)  │
       └──────────────────┘

Подробное описание компонентов и потоков данных — в ARCHITECTURE.md.

Quick Start (5 минут)

git clone https://github.com/kiskameow/dental-clinic-telegram-bot.git
cd dental-clinic-telegram-bot

cp .env.example .env
# Отредактируй .env: PATIENT_BOT_TOKEN, ADMIN_BOT_TOKEN, ADMIN_TELEGRAM_IDS
# Получи токены у @BotFather

docker compose up -d                                          # postgres, redis, api, patient-bot, admin-bot, worker
docker compose run --rm api alembic upgrade head              # применить миграции
docker compose run --rm api python -m scripts.seed_demo       # заполнить demo-данными

Бот готов. Откройте Telegram, найдите своего бота, отправьте /start.

Для production-разворачивания (Caddy + TLS + Sentry + бэкапы + healthchecks) — см. DEPLOY.md.

Структура проекта

stomatology-booking-bot/
├── src/booking_bot/
│   ├── api/                    # FastAPI: /healthz, /readyz, future webhooks
│   ├── bots/
│   │   ├── patient/            # patient bot entry-point
│   │   ├── admin/              # admin bot entry-point + handlers
│   │   ├── handlers/           # patient handlers: booking, consent, ai, cancel, reschedule
│   │   ├── middlewares/        # admin_auth, admin_session, consent, rate_limit
│   │   └── factory.py          # bot dispatcher factory
│   ├── domain/                 # бизнес-логика (без I/O, без БД)
│   │   ├── booking.py          # booking + race condition pattern
│   │   ├── cancel.py
│   │   ├── reschedule.py
│   │   ├── consent.py          # 156-ФЗ flow
│   │   ├── slot_generator.py
│   │   ├── ai_router.py        # intent classification
│   │   ├── ai_faq.py           # FAQ orchestration с pipeline
│   │   ├── ai_guardrails.py    # 476 строк защиты (input + output)
│   │   ├── ai_circuit_breaker.py
│   │   ├── ai_client.py        # GigaChat wrapper
│   │   ├── ai_errors.py        # AIInputRejectedError, AIOutputRejectedError, etc.
│   │   ├── rate_limiter.py
│   │   ├── prompt_loader.py
│   │   ├── config_schemas.py   # Pydantic strict validation для clinic.yaml
│   │   └── enums.py
│   ├── db/
│   │   ├── models.py           # 10 SQLAlchemy 2.0 моделей
│   │   ├── engine.py
│   │   └── repositories/       # 9 repository-модулей (по одному на агрегат)
│   ├── notifications/          # telegram + email_smtp + base
│   ├── workers/
│   │   └── reminder.py         # SELECT FOR UPDATE SKIP LOCKED loop
│   ├── app_context.py          # composition root
│   └── config.py               # env settings
├── config/
│   ├── clinic.example.yaml     # шаблон для нового клиента
│   └── ulybka-kazan.yaml       # demo: «Стоматология Улыбка Казань»
├── prompts/
│   ├── intent_router.txt       # AI-роутер system prompt
│   └── clinic_faq.txt          # FAQ system prompt с hard-rules + repeat anti-injection
├── legal/
│   ├── consent_pd_v1.html      # 156-ФЗ согласие, source
│   └── consent_pd_v1.pdf       # rendered, hashed at consent:agree
├── scripts/
│   ├── generate_consent_pdf.py # WeasyPrint render
│   └── seed_demo.py            # demo-данные
├── tests/
│   ├── unit/                   # 43 теста
│   └── integration/            # 17 тестов (Postgres + Redis через testcontainers)
├── alembic/versions/           # миграции
├── .github/workflows/          # CI: pytest + ruff + mypy
├── docker-compose.yml          # 5 services
├── Dockerfile
├── pyproject.toml
├── ARCHITECTURE.md             # подробная архитектура
├── DEPLOY.md                   # production-deploy
└── ADD_CLINIC.md               # rebrand под нового клиента

Production-grade паттерны

Это template, который применяет осознанно отобранные production-практики, а не «работает на моей машине».

Race condition protection

Двойная запись на один слот — самый частый production-баг booking-ботов. Решено через pg_advisory_xact_lock(doctor_id, date_int) + SELECT ... FOR UPDATE в одной транзакции:

# domain/booking.py — упрощённая версия
async with session.begin():
    await session.execute(
        text("SELECT pg_advisory_xact_lock(:doctor_id, :date_int)"),
        {"doctor_id": doctor_id, "date_int": int(date.strftime("%Y%m%d"))},
    )
    slot = await session.scalar(select(Slot).where(...).with_for_update())
    if slot.taken_by_appointment_id:
        raise SlotAlreadyTakenError
    # commit on async with exit

100 параллельных booking-запросов на один слот — гарантированно одна успешная запись, остальные получают SlotAlreadyTakenError. Покрыто tests/integration/test_concurrent_booking.py.

Reminder worker — durable очередь

APScheduler молча теряет jobs при рестарте. Вместо него:

  1. Каждое booking создаёт строки в reminder_queue (reminder_at, channel, appointment_id)
  2. Worker опрашивает таблицу каждые 60 секунд через SELECT ... FOR UPDATE SKIP LOCKED LIMIT 100
  3. Несколько worker-инстансов могут работать параллельно — SKIP LOCKED гарантирует уникальную выдачу
  4. Retry с подсчётом attempts, max 5, last_error в БД для дебага

Pattern переживает рестарты, scaling, долгие GIL-локи, всё.

156-ФЗ согласие отдельным документом

С 1 сентября 2025 (ФЗ №156-ФЗ от 24.06.2025) согласие на обработку ПД обязано быть отдельным документом. Штраф до 700 000 ₽.

Реализация:

  • При первом взаимодействии бот шлёт PDF-согласие отдельным сообщением
  • Кнопка «Я согласен» — отдельный callback (не объединена с другими подтверждениями)
  • При клике пишется строка в consent_log: telegram_id + consent_version + SHA-256 хэш PDF + consent_type + created_at
  • Документ хэшируется при каждой даче согласия, не на старте — это гарантирует, что подмена PDF на хосте сразу видна в audit log
  • При смене версии PDF — пересогласование у всех пользователей

Безопасность AI

domain/ai_guardrails.py (476 строк) — defense-in-depth для AI-FAQ:

Pre-LLM (input):

  • Структурные: control chars, zero-width chars (token smuggling), prompt-tag injection (<|im_start|>, etc.), HTML/XML tags, markdown system-prompt injection (### System)
  • Эвристики: base64-blob detection (40+ chars), URL detection (любой URL = подозрительно), excessive newlines, char floods (30+ same char)
  • Паттерны: 5 семейств jailbreak (EN + RU), code injection, refusal bypass (в виде исключения, гипотетически), prompt extraction
  • Operator-supplied custom patterns с fail-soft compile (broken pattern → warning, не crash)

Post-LLM (output):

  • instruction_bypass markers первыми — фингерпринт компромисса, чтобы не маскировался другими guard’ами
  • Forbidden patterns: «гарантирую», «100%», «противопоказаний нет» (RU consumer-protection law)
  • Medical advice: диагнозы (у вас кариес), назначения препаратов
  • Price authorization: response может содержать только цены из allowed_prices (загружаются из конфига)
  • Phone authorization: только клиники, не любые
  • URL в response, code blocks
  • System prompt echo (verbatim) + 60-char sliding-window overlap (paraphrase attacks)
  • Cyrillic ratio ≥ 0.7 для enforce_russian: true

Defense layers:

  • Circuit breaker (5 consecutive failures → open 5 min) — не сжигаем токены и не лагуем UX при backend-проблемах
  • Per-clinic hourly rate limit — guard от runaway cost
  • Audit log для каждого AI-call (input, output, is_safe, rejection_reason) — всегда пишется, fail-safe (если БД недоступна — log и не падаем)

Все защиты протестированы в tests/integration/test_ai_adversarial.py (30 KB end-to-end тестов с реальными jailbreak-промптами).

Прочее

  • Soft delete для пациентов (deleted_at) — мягкое удаление + право на забвение по 152-ФЗ
  • Naming convention для Alembic constraints — установлен с первой миграции, чтобы автогенерация имён была детерминированной
  • N+1 prevention через selectin loading в моделях appointments
  • Idempotent seed-script — можно запускать повторно без падений
  • structlog с JSON-output — structured logging готов под Loki/Grafana

Testing

docker compose run --rm api pytest                  # все 60 тестов
docker compose run --rm api pytest -m "not integration"  # только unit
docker compose run --rm api pytest --cov            # coverage
Тип Кол-во Что проверяют
Unit 43 Чистые функции, валидация, FSM-states, handlers с моками
Integration 17 Real Postgres + Redis через testcontainers
Adversarial (subset of integration) 1 (но 30 KB) End-to-end атаки на AI: jailbreak, prompt-injection, price-spoofing

Ключевые сценарии:

  • test_concurrent_booking.py — 100 одновременных booking на один слот
  • test_concurrent_cancel.py — race на отмене
  • test_consent_flow_e2e.py — полный 156-ФЗ flow с audit log
  • test_migration_roundtrip.py — каждая миграция up + down
  • test_pdf_render.py — PDF-генерация
  • test_reminder_worker.pySKIP LOCKED поведение
  • test_ai_adversarial.py — все слои AI safety

Configuration

Бот config-driven. Один YAML-файл (config/<clinic-code>.yaml) описывает клинику целиком: услуги, врачей, расписание, FAQ, согласия. Pydantic-схема (domain/config_schemas.py) валидирует:

  • Cross-references: каждый doctor.services существует в services
  • Уникальность кодов услуг и врачей
  • work_start < work_end
  • Хорошо-сформированные коды (regex ^[a-z][a-z0-9-]*$)
  • Лимиты длины и значений

Развёртывание под нового клиента — см. ADD_CLINIC.md. Время — 1-2 часа на rebrand.

Deployment

Production-deploy с Caddy + Let’s Encrypt + Sentry + бэкапы — см. DEPLOY.md.

Development

# Install (Python 3.13 required)
pip install -e ".[dev]"

# Lint + type check
ruff check src/ tests/
mypy src/booking_bot/

# Run locally (без Docker)
DATABASE_URL=postgresql+asyncpg://booking:booking@localhost:5432/booking \
REDIS_URL=redis://localhost:6379/0 \
python -m booking_bot.bots.patient

CI (.github/workflows/) запускает pytest, ruff, mypy на каждый push.

Roadmap

  •  Day 1-2: Foundation, схема БД, 156-ФЗ consent flow
  •  Day 3: Booking flow + race condition protection
  •  Day 4: Reminder worker + email backend
  •  Day 5: Admin bot + management/reports
  •  Day 6: Cancel/reschedule + middlewares
  •  Day 7: AI router + AI FAQ + guardrails + circuit breaker + rate limit + audit log
  •  Day 8: Production deploy (Caddy, TLS, Sentry, backups), email reminder enable
  •  Day 9: Loom walkthrough video, public live demo
  •  Day 10: Case study / VC.ru article

License

MIT — см. LICENSE.

Demo

🌐 Live demo URL: coming after Day 8 deploy 🎬 7-minute video walkthrough: in production this week 🤖 Try the patient bot: link added after deploy

Until the live demo is up, docker compose up -d gives you a fully working patient + admin bot in under 5 minutes. Use the demo config config/ulybka-kazan.yaml — three services, two doctors, 30 days of generated slots.

Need this for your clinic?

Three paths forward — pick based on your tech comfort and timeline.

1. Fork & deploy yourself

Code is MIT-licensed. Fork the repo, edit config/<your-clinic>.yaml, follow ADD_CLINIC.md. Full production setup in DEPLOY.md. Best fit if you have an in-house developer or sysadmin who can read the docs and own deployment.

2. I deploy under your clinic

What’s included:

  • Full deployment on your VPS (or my managed infrastructure)
  • Custom 156-ФЗ consent PDF generated with your legal entity’s data
  • Configured both bots, reverse proxy with TLS, daily backups, monitoring + Telegram alerts
  • Production smoke-test (end-to-end booking + reminder + cancel + admin operations)
  • Recorded walkthrough specific to your configuration
  • Handover documentation so any developer can continue maintenance

Indicative delivery: 1-3 days after access. Final price depends on scope (single location vs multi-location, custom integrations).

3. Monthly retainer (after launch)

Ongoing support:

  • 4-hour response time (business days), 12-hour off-hours
  • Library security updates, key rotation, dependabot PRs reviewed
  • Reasonable config changes (new doctors, services, schedule updates)
  • Quarterly health-check + AI audit log review

Pricing by tier — contact for current rates.

Contact

Hire inquiries and technical questions both welcome.


Production-grade Telegram bot template for Russian SMB. Companion templates (B2B portal for dental labs, secure corporate channel setup) in development.

Описание
Конвейеры
0 успешных
0 с ошибкой
Разработчики