Telegram-бот, который заменяет первичный получасовой созвон HR с каждым кандидатом на пятиминутное автоматическое интервью. Кандидат проходит 5 вопросов в чате, LLM оценивает ответы по критериям вакансии, и каждый кандидат строкой попадает в Google Sheets с цветовой меткой и кликабельной ссылкой на чат. Portfolio piece.
Node 20 · Telegraf 4 · Groq (Llama 3.3 70B) · PostgreSQL · Redis · Google Sheets API · PDFKit
Source · Changelog · Contributing · Security
HR-BOT — портфолио-проект: автоматизированная воронка первичного скрининга, в которой вся работа рекрутёра сводится к открытию знакомой Google-таблицы. Кандидат заходит в Telegram-бота по deep-link с вакансии, проходит интервью с прогрессом, LLM оценивает каждый ответ по criteria вопроса, и в Sheets сразу появляется готовая к фильтрации строка: имя, балл, рекомендация, цвет, ссылка на чат.
Каждый модуль, миграция, тест и блок документации написан вручную: ни ноу-кодов, ни copy-paste из туториалов, ни заглушек «MVP в выходные». Стек как у боевого Telegram-сервиса, но компактный — один бот, один Postgres, один Redis, одна таблица. Все «production must-have» — health-эндпоинт, idempotent миграции, rate-limit, drop-pending-updates, dependabot, coverage в CI — закрыты и проверяются на каждом пуше.
Disclaimer. Демо-вакансии, вопросы и критерии оценки (
data/vacancies.json) — fictional и подобраны исключительно как стартовый набор для портфолио. На любую реальную вакансию переезжается за минуту через/add_vacancy. Сам код переносится на любой Telegram-канал найма без правок.Designed and built by Тимур Валерьевич.
Companion-проекты. Этот бот — backend-кусок моего портфолио. Соседние работы:
- Один
/start— и поехали. Бот выводит список активных вакансий с коротким описанием под каждой кнопкой; deep-linkt.me/<bot>?start=vac_<id>сразу открывает нужную позицию. - Прогресс-бар на каждом шаге.
Вопрос 2 из 5 ▓▓░░░— кандидат видит, сколько ещё осталось, и не теряет интерес. - Resume интервью. Если кандидат закрыл Telegram посередине, новый
/startпредлагает «Продолжить с вопроса N» или «Начать заново». - Скрытые оценки. Балл, рекомендация и AI-резюме видит только рекрутёр. Кандидат не подстраивается под систему — отвечает честнее.
- «Печатает…» во время LLM-вызова.
sendChatActionпоказывает индикатор пока Groq оценивает ответ, чтобы кандидат не подумал, что бот завис. - Ротация подтверждений. «Принято, спасибо», «Понял, идём дальше», «Записал. Следующий:» — пять фраз чередуются, диалог не выглядит шаблонно.
- Минимальная длина ответа. 20 символов на ответ + понятная ошибка с показом текущей/требуемой длины.
/cancelи/help. Кандидат может прерваться в любой момент./helpпоказывает справку, рекрутёру дополнительно подгружаются команды админа в персональный chat scope черезsetMyCommands.setMyDescription+setMyShortDescription— карточка бота в Telegram выглядит профессионально без ручного захода в BotFather.
- Google Sheets как UI. Главная фича — лист «Кандидаты» открывается первым, дефолтный «Лист1» удаляется автоматически. Жирный цветной заголовок, замороженная первая строка, банды для нечётных рядов, ширины колонок выставлены под содержимое.
- Условное цветовое кодирование. «Нанять» — зелёный, «Доп.интервью» — жёлтый, «Отказать» — красный. Глазом находишь топ-кандидатов за секунду, фильтры/сортировка не нужны.
- Кликабельная колонка «Контакт».
=HYPERLINK("https://t.me/<user>","Открыть чат →")— один клик и ты в диалоге с кандидатом. - Лист «Статистика» с формулами. Воронка (всего/за неделю/сегодня), средний балл, разбивка по вакансиям, ТОП-5 кандидатов через
QUERY. Всё считается формулами, бот не пересчитывает значения сам. - Кириллический PDF-отчёт. Roboto-Regular/Bold embed (никаких квадратов на месте русских букв), цветной хедер, бэйдж рекомендации, номера страниц. Отправляется опционально на
RECRUITER_EMAILи всегда приходит админу прямо в Telegram файлом. - Опциональный email. Без
SMTP_*бот тихо пропускает рассылку — PDF лежит локально вreports/. С SMTP (Gmail App Password / Mailgun / любой) — улетает рекрутёру вместе со ссылкой на Sheets. /resultsс пагинацией.◀ Назад / Вперёд ▶, заголовок «Интервью 11–20 из 47 (стр. 2/5)»./search <имя>— быстрый поиск по имени кандидата через trigram-индекс (pg_trgm)./statsс текстовыми барами. Воронка отображается без графиков и зависимостей —██████░░░░рядом с цифрой./vacancies,/add_vacancy,/export. Список активных вакансий с кнопками деактивации; добавление новой вакансии в один paste; ссылка на Sheets.
- HTTP
/health+/healthzна отдельном портуHEALTH_PORT(8081). Возвращает{status, uptime, groq, sheets}. Готово для DockerHEALTHCHECK, Kubernetes liveness/readiness, Railway/Render healthcheck. - Авто-миграции + idempotent сидинг при старте. Контейнер на любом PaaS пересоздаётся —
runMigrations()прогоняетmigrations/*.sql(DDL все сIF NOT EXISTS),seedVacanciesIfEmpty()загружает 5 демо-вакансий только если БД пустая. - PII-редакция в логах. Имена кандидатов и handle маскируются (
Иван Петров→И*** П***,@vasya→@v***) перед записью в Railway-логи и Sentry. - Rate-limit
/startчерез Redis: 5 нажатий в минуту на user_id, защита от спама. dropPendingUpdates: trueпри запуске — больше нет 409 Conflict в логах после деплоя Railway.- Groq retry-on-fail. Один повтор при
5xxс экспоненциальным бэкоффом. - Webhook режим опционально. По умолчанию long-polling, для production-scale
WEBHOOK_URL+WEBHOOK_SECRET. - Pre-commit hooks — Husky + lint-staged: Prettier и ESLint фиксят файлы до коммита.
- CI matrix на каждый PR:
format:check→lint→typecheck→build→test→coverage(артефакт сохраняется 14 дней). - Dependabot — еженедельно для npm (grouped dev/prod), ежемесячно для GitHub Actions и Docker. Лимит 5 open PR.
┌───────────────────────────┐
│ Telegram (BotFather API) │
└─────────────┬─────────────┘
│ long-polling / webhook
▼
┌────────────────────────────────────────────────────────┐
│ src/bot.ts (Telegraf 4) │
│ /start · /help · /cancel · rate-limit · setMyCommands │
└──────────┬─────────────────────────────────┬───────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ src/interview.ts │ │ src/admin.ts │
│ FSM: ready → in_prog │ │ /vacancies /stats │
│ → done · resume │ │ /results /search │
│ typing-indicator │ │ /add_vacancy /export │
└──────────┬──────────────┘ └────────────┬────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ src/evaluator.ts · src/reporter.ts │
│ Groq llama-3.3-70b · JSON-mode · retry · Sheets API · PDF │
└──────────┬───────────────────────────────────┬───────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌─────────────────────────────┐
│ Postgres (db.ts) │ │ Redis (session.ts) │
│ vacancies │ │ state machine · TTL 3600s │
│ questions │ │ rate-limit counters │
│ interviews + idx │ │ │
└──────────────────────┘ └─────────────────────────────┘
| Слой | Инструмент |
|---|---|
| Runtime | Node.js 20 LTS, TypeScript 5 (strict) |
| Telegram | Telegraf 4 (setMyCommands, MarkdownV2, deep-link) |
| LLM | Groq SDK — llama-3.3-70b-versatile, JSON response_format |
| Persistence | PostgreSQL 16 + pg + pg_trgm |
| Session state | Redis 7 + node-redis |
| Sheets | googleapis v4 (formatting, conditional rules, HYPERLINK) |
| pdfkit + Roboto Cyrillic TTF | |
| nodemailer (Gmail / Mailgun / любой SMTP) | |
| Validation | zod — runtime-проверка env |
| HTTP | node:http — /health + webhook (без Express) |
| Tests | Vitest 2 + @vitest/coverage-v8 (36 кейсов) |
| Lint / style | ESLint 9 + Prettier + Husky + lint-staged |
| CI | GitHub Actions (format:check → lint → typecheck → build → test → coverage) |
| Container | node:20-alpine + Docker Compose (Postgres + Redis + bot, healthcheck) |
| Hosting | Railway / Render / any PaaS |
git clone https://github.com/timur123-star/HR-BOT.git
cd HR-BOT
cp .env.example .env
# минимум — TELEGRAM_BOT_TOKEN. Опционально GROQ_API_KEY, GOOGLE_*, SMTP_*.
docker compose up --buildPostgres и Redis поднимутся как сервисы compose, бот сам прогонит миграции и засидит 5 демо-вакансий при первом старте.
- Подключить репо и выставить переменные окружения из
.env.example. - Railway сам поднимет Postgres и Redis из marketplace; бот при первом запуске прогонит
migrations/*.sqlи засидитdata/vacancies.json. - Открыть
https://<service>.up.railway.app/healthдля проверки.
npm install
npm run migrate
npm run seed
npm run dev/start— выбрать вакансию и начать интервью./start vac_<id>— deep-link на конкретную вакансию./cancel— прервать текущее интервью./help— справка.
/vacancies— активные вакансии с кнопками «Деактивировать»./add_vacancy— добавить новую вакансию в форматеTitle | Description\nQ1: текст | критерии\n…./results— пагинированный список интервью (по 10, кнопки◀ / ▶)./search <часть имени>— поиск по кандидатам (pg_trgmILIKE)./stats— воронка с текстовыми барами и средним баллом./export— кнопка-ссылка на Google Sheets.
- https://console.cloud.google.com → новый проект → APIs & Services → Library → Google Sheets API → Enable.
- Credentials → Create Credentials → Service account → имя любое → роль пропустить → Done.
- Кликнуть на созданный аккаунт → Keys → Add Key → Create new key → JSON → сохранить файл.
- Содержимое файла одной строкой положить в
GOOGLE_CREDENTIALS_JSON(или путь к файлу — вGOOGLE_CREDENTIALS_FILE). - Создать пустую Google-таблицу → Share с email из
client_email(роль Editor). - ID таблицы (между
/d/и/editв URL) положить вGOOGLE_SHEET_ID.
Бот сам создаст листы «Кандидаты» и «Статистика», применит форматирование, conditional rules и формулы — без ручной настройки шаблона.
src/
bot.ts # entrypoint: миграции, seed, launch (polling / webhook)
config.ts # zod-валидация env
db.ts # Postgres операции + searchInterviewsByName
format.ts # escapeMd / progressBar / textBar / formatDate / redactPii
health.ts # HTTP /health + /healthz (node:http)
interview.ts # FSM интервью: state machine, typing-indicator, ротация
evaluator.ts # Groq evaluation + summary с retry
reporter.ts # Sheets layout + append + PDF + email
admin.ts # /vacancies /stats /results /search /add_vacancy /export
ratelimit.ts # Redis-based rate-limit
session.ts # Redis state с TTL 3600s
startup.ts # idempotent migrations + seed
logger.ts # structured JSON logger + PII redaction
types.ts # shared TS types
migrations/
001_init.sql # vacancies / questions / interviews + UNIQUE
002_indexes.sql # pg_trgm + composite + partial indexes
data/
vacancies.json # 5 демо-вакансий с критериями
assets/
Roboto-*.ttf # Cyrillic шрифты для PDF
tests/ # 36 unit-тестов на критическую логику
.github/
workflows/ci.yml # format → lint → typecheck → build → test → coverage
dependabot.yml # weekly npm, monthly GH Actions / Docker
ISSUE_TEMPLATE/ # bug_report.md, feature_request.md
scripts/
migrate.ts # ручной запуск миграций
seed.ts # ручной запуск сидера
share.ts # генератор deep-link: npm run share -- 2
- Sheets — главный UI, не Telegram. Рекрутёр не хочет учить новый интерфейс. Google Sheets — общий язык: фильтры, сортировка, шаринг, копи-паст в Notion — всё это уже есть бесплатно.
- Скрытые оценки. Если кандидат видит балл, он подстраивается под систему. Скрытая оценка → ответы ближе к реальности → LLM судит по сути.
- Groq, а не OpenAI. Free tier, sub-секундная latency на 70B, JSON-mode из коробки. Заменяемо одной строкой в
evaluator.ts. - Idempotent seed. На Railway контейнер пересоздаётся при каждом деплое —
seedVacanciesIfEmptyпроверяет, что таблица пуста, и только тогда вставляет демо. Свои добавленные через/add_vacancyникогда не перезатираются. - UNIQUE
(candidate_tg, vacancy_id). Кандидат не может пройти ту же вакансию дважды — попытка возвращает дружелюбное сообщение, в БД не плодятся дубликаты. - PII-редакция в логах. Имя/handle/телефон маскируются перед уходом в stdout / Sentry. Логи Railway безопасно показывать в портфолио.
- Health-эндпоинт —
node:http, без Express. Минимум зависимостей, ~0.1 ms ответ.
npm run format # prettier --write
npm run format:check # prettier check (первый шаг CI)
npm run lint # eslint strict
npm run typecheck # tsc --noEmit
npm test # vitest run — 36 unit-тестов
npm run test:coverage # vitest + v8 coverage (HTML + lcov + json-summary)
npm run build # tsc → dist/GitHub Actions запускает всё на каждый PR. Husky pre-commit прогоняет prettier --write + eslint --fix на стейджнутых файлах через lint-staged — CI ловит только то, что хук пропустил.
npm run share -- 2
# → https://t.me/<your_bot>?start=vac_2По умолчанию бот работает в long-polling. Для production-scale задать WEBHOOK_URL + WEBHOOK_SECRET — бот поднимет HTTP-сервер на WEBHOOK_PORT (дефолт 8080) и зарегистрирует webhook в Telegram.
MIT. Roboto-Regular / Roboto-Bold — Apache 2.0.
Designed and built by Тимур Валерьевич.