HACKALEM AI — умный подбор подрядчиков
Веб-приложение, которое по городу, дате, категории, формату и бюджету мероприятия подбирает до трёх подрядчиков из каталога (66 профилей), объясняет…
О проекте
Команда: BELES
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Веб-приложение, которое по городу, дате, категории, формату и бюджету мероприятия подбирает до трёх подрядчиков из каталога (66 профилей), объясняет выбор или сообщает причину отказа.
Как устроено
CSV импортируется скриптом в JSON-каталог. Сервер на Node.js фильтрует по городу, категории, занятости на дату, бюджету с допуском 10%, формату, языку и часам. Ранжирование детерминированное: 100 баллов за бюджет, +10 за совпадение корня формата в описании, +5 за язык, при равенстве решают цена и ID. Топ-5 уходит в OpenAI Responses API (gpt-4.1-mini, Structured Outputs, temperature 0), модель пишет объяснения для фиксированных трёх ID, сервер проверяет ID и шаблонность текста. При ошибке или отсутствии ключа строятся локальные объяснения с цитатой из профиля. Отдельные исходы no_category и no_match.
Ключевые решения
- hard-constraint filtering
- deterministic heuristic ranking
- Structured Outputs
- LLM response validation
- local fallback explanations
Особенности
LLM не может изменить состав и порядок выдачи, только пишет объяснения для ID, заданных сервером. Для жюри подготовлены воспроизводимые сценарии: смена даты меняет выдачу, редкая категория, все заняты, нет категории.
Стек
React 19 · TypeScript · Vite 8 · Node.js 24 · Docker · Caddy
Модели
gpt-4.1-mini · Gemini (ранняя версия)
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-5a75f2ac-beles.
README
README.mdHACKALEM AI — умный подбор подрядчиков
Решение задачи HackAlem #79-lite: до трёх подходящих подрядчиков с конкретными
объяснениями из анонимизированного каталога. Без бронирования и отправки заявок.
React + TypeScript + Node.js 24, OpenAI Responses API (gpt-4.1-mini).
Организатор указывает город, дату, категорию, формат и бюджет мероприятия. Приложение проверяет ограничения каталога, показывает подходящие профили и объясняет рекомендации либо сообщает, почему подобрать подрядчика не удалось.
Что реализовано
- Форма поиска с дополнительными условиями по языку и длительности.
- Проверка города, категории, занятости, бюджета с допуском 10%, формата, языка и часов.
- Воспроизводимое ранжирование и до трёх результатов с объяснениями OpenAI.
- Корректная выдача для редких категорий и отказы с причинами при отсутствии вариантов.
- Локальные объяснения с явной пометкой при недоступности OpenAI или отсутствии ключа.
- Кликабельные карточки с переходом к полному профилю и демонстрационная карта Казахстана.
- Импорт исходного CSV, автоматические тесты API, Windows-скрипт запуска, Docker и Caddy.
Технологии и архитектура
| Часть | Реализация |
|---|---|
| Интерфейс | React 19, TypeScript 5, Vite 8, CSS и SVG-карта |
| Сервер | Node.js 24, встроенный HTTP-сервер, POST /api/match |
| Данные | CSV в репозитории → скрипт импорта → локальный JSON-каталог |
| ИИ | OpenAI Responses API, по умолчанию gpt-4.1-mini, Structured Outputs |
| Запуск | Node.js напрямую или контейнер приложения и Caddy как reverse proxy |
| Проверки | Встроенный тестовый раннер Node.js и TypeScript |
Браузер отправляет форму серверу. Сервер читает каталог, применяет фильтры и фиксирует
первые три профиля. OpenAI получает параметры запроса и до пяти кандидатов и пишет
объяснения для выбранных ID. Сервер проверяет ответ и возвращает MatchResponse.
Если ИИ недоступен, тот же набор профилей получает локальные объяснения.
Ключ используется только сервером. Отдельная база данных не требуется: каталог хранится в файлах. Компонент карты не выполняет API-запросов.
Установка и запуск
Нужны Node.js 24 с npm, Git и интернет для установки зависимостей. Для объяснений ИИ дополнительно нужен ключ OpenAI с доступом к модели и API-кредитами. Без ключа можно проверить фильтры, выдачу и отказы.
Скачайте рабочую ветку проекта:
git clone --branch master https://github.com/BAITC-Hacks/hack-5a75f2ac-beles.git hackalem-ai
cd hackalem-ai
Если нет доступа к репозиторию хакатона, тот же код находится в публичном зеркале: purasu08-droid/Qadam, ветка master.
Быстрый запуск на Windows
- В корне проекта скопируйте
.env.exampleв.env. - Для ИИ заполните
OPENAI_API_KEYв.env. Для локального режима оставьте поле пустым. - Дважды нажмите
start.cmd. - Дождитесь сообщения
Open http://localhost:3000и откройте этот адрес. - Выберите пример запроса или заполните форму и нажмите «Найти подрядчиков».
- Окно запуска оставьте открытым. Для остановки —
Ctrl+C.
Первый запуск устанавливает зависимости и собирает интерфейс. Нужен интернет. Используется Node.js 24 из Codex или обычная установка с https://nodejs.org.
Без API-ключа подбор тоже работает. Объяснения с цитатами из профилей составляются
локально и отмечаются в интерфейсе как «По данным каталога». Это не ответы ИИ.
Чтобы включить OpenAI, в созданном .env укажите OPENAI_API_KEY и перезапустите
приложение. Ключ можно создать на https://platform.openai.com/api-keys в проекте,
которому доступны ваши API-кредиты. Ключ не нужно присылать в чат.
Скрипт запуска использует доверенные системные сертификаты (--use-system-ca),
чтобы HTTPS-запросы к OpenAI работали и в сетях с установленным корпоративным
сертификатом. Проверка HTTPS остаётся включённой.
Если порт 3000 уже занят предыдущим запуском, используйте работающую страницу или остановите предыдущее окно перед повторным запуском.
Данные и внешние сервисы
data/hackathon-contractors.csv— неизменённый исходный CSV.data/contractors.json— 66 профилей: 50 в Алматы, 15 в Астане, 1 в локации «Зарубежье».- 17 категорий, 6 форматов, 3 языка; 13 профилей помечены
synthetic: true. - Имена анонимизированы; пометка «Синтетическая запись» видна на соответствующих карточках.
city_imputedиprice_imputedсохранены: карточки поясняют, когда город или цена были проставлены при подготовке датасета.- У 9 профилей
max_hours: null: работа не привязана к присутствию на площадке; фильтр длительности их не исключает. - Календарь действует 23.09.2026–31.12.2026. Даты вне окна не принимаются.
- Списки из CSV разделяются по
|; цены и часы преобразуются в числа, флаги — в boolean.
Воспроизводимый импорт без дополнительных библиотек:
node scripts/import-contractors.mjs
Он проверяет схему, уникальность ID, числа, даты и флаги, создаёт JSON каталога
и src/data/catalog-options.json для формы. Исходный CSV остаётся без изменений.
После повторного импорта пересоберите интерфейс. Карта — отдельная UI-демонстрация
с восемью демонстрационными маркерами и фильтрами по типу; в подборе участвуют только 66 записей каталога.
Карточки результатов и всплывающие карточки карты кликабельны: «Подробнее о подрядчике» прокручивает страницу к полному описанию, форматам, языкам, длительности и занятым датам. Кнопка «Назад к карточке» возвращает к исходной карточке. Профили карты сопоставляются с локальным каталогом по имени и городу; у демонстрационных точек без совпадения показываются только имеющиеся сведения с явной пометкой, без выдуманной доступности.
До поиска отображаются три ознакомительных профиля каталога; их доступность ещё не
проверена по условиям. Выбор категории в форме меняет эту подборку.
Результаты поиска заменяют ознакомительную подборку.
Карта находится в самом низу, после результатов и открытого подробного профиля.
Страница начинается с формы поиска под навигацией; фон оформлен синим пиксельным узором.
Карточки и подробные профили используют сгенерированные изображения с пометкой
«ИИ-иллюстрация». Это повторно используемые образы профессий и площадки, а не реальные
фотографии подрядчиков. Изображения закреплены за категориями и ID, поэтому не меняются
при повторном поиске. Исходные промпты: src/assets/contractors/README.md.
Во время подбора единственный внешний API — OpenAI. Ему передаются параметры
поиска и анонимизированные профили кандидатов; ключ хранится в локальном .env.
При пустой выдаче вызова ИИ нет. Изображения уже включены в репозиторий и во время
поиска не генерируются. Google Gemini и NVIDIA API в текущем коде не используются.
Как работает подбор
- Сервер проверяет обязательные город, дату, формат, категорию и числовой бюджет. Язык и длительность необязательны.
- Находит категорию в выбранном городе. Если профилей нет — исход
no_category. - Исключает занятых на дату, не поддерживающих формат/язык/длительность и тех, чья стартовая цена выше бюджета более чем на 10%. Допуск сохранён по условиям проекта; превышение бюджета обязательно отмечается в объяснении.
- Если никто не прошёл, возвращает
no_matchс причинами и числом исключений. Один профиль может не пройти несколько условий, поэтому счётчики причин не суммируются. - Ранжирует детерминированно: в пределах бюджета — 100 баллов; каждое совпадение тематического корня формата в описании — 10; упоминание запрошенного языка — 5. При равенстве — меньшая цена, затем ID. Это простая объяснимая эвристика, не эмбеддинги.
- Берёт топ-5 для контекста, фиксирует первые 3 на сервере и просит OpenAI написать 1–2 конкретных предложения для каждого выбранного ID. Модель не меняет состав и порядок.
- Проверяет ID, повторы и пустые/шаблонные объяснения. Возвращает исходные объекты подрядчиков; ответ модели не может подменить цену или календарь.
При matched показывается до трёх карточек: имя, категории, город, цена, происхождение
профиля и объяснение. Если подходящих меньше трёх — показываются все и причина.
Пустые исходы объясняются сервером без обращения к ИИ; HTTP 200 отличает их от технических ошибок.
temperature: 0 используется для объяснений, но гарантия порядка обеспечивается
серверным ранжированием, а не этой настройкой. Для одной версии каталога одинаковый
запрос даёт одинаковые ID в том же порядке, даже если ИИ вернул объяснения в другом порядке.
Вызов ИИ ограничен 7,5 секундами и 1200 выходными токенами, store: false.
При отсутствии ключа, ошибке API или плохом ответе используются локальные объяснения:
проверенные условия плюс конкретная цитата из description. Источник виден пользователю.
Это позволяет сохранить рабочий подбор при недоступности ИИ; реальную задержку API
нужно измерять со своим ключом. Общий лимит сервера — 20 поисков в минуту.
Демонстрация для жюри
Во всех примерах язык — русский, длительность не задана.
Для трёх основных сценариев нажмите соответствующую карточку в блоке «Можно начать с идеи», затем «Найти подрядчиков». При рабочем OpenAI в карточках появляется «Объяснение ИИ»; «По данным каталога» означает локальный режим.
| Сценарий | Город / категория | Формат | Дата | Бюджет | Ожидаемый результат |
|---|---|---|---|---|---|
| Плотная категория | Алматы / Ведущий | корпоратив | 06.10.2026 | 1 000 000 ₸ | 3 из 6 подходящих |
| Изменение даты | Алматы / Ведущий | корпоратив | 07.10.2026 | 1 000 000 ₸ | 3, состав меняется из-за календаря |
| Редкая категория | Алматы / Флорист | свадьба | 23.09.2026 | 300 000 ₸ | 2, объяснение почему меньше трёх |
| Все заняты | Алматы / Флорист | свадьба | 25.12.2026 | 300 000 ₸ | no_match, оба заняты |
| Нет категории | Зарубежье / Ведущий | корпоратив | 06.10.2026 | 3 000 000 ₸ | no_category |
Повторите первый запрос: порядок остаётся HK-88430, HK-44733, HK-44923.
На 7 октября HK-44733 занят; выдача — HK-88430, HK-44923, HK-29829.
В карточках и сводке показана дата/занятость, так что изменение можно объяснить.
Форма проверяет одну выбранную дату, а не диапазон. Для флористов Алматы при условиях из таблицы оба профиля заняты 25, 27 и 30 декабря. На 26, 28 и 31 декабря есть по одному подходящему профилю, на 29 декабря — два. Нельзя утверждать, что все подрядчики заняты весь период 25–31 декабря: результат зависит от даты и категории.
Проверка API без интерфейса (PowerShell):
$request = @{
city = 'Алматы'
category = 'Ведущий'
date = '2026-10-06'
format = 'корпоратив'
budget = 1000000
language = 'русский'
} | ConvertTo-Json
Invoke-RestMethod -Uri 'http://127.0.0.1:3000/api/match' -Method Post `
-ContentType 'application/json; charset=utf-8' `
-Body ([System.Text.Encoding]::UTF8.GetBytes($request))
Ожидаются status: matched, три элемента matches и summary.eligible: 6.
explanation_source: openai подтверждает объяснения модели, local — запасной режим.
Наличие HTTP 200 само по себе не подтверждает работу ИИ.
Проверка и разработка
Для ручного запуска после скачивания проекта скопируйте .env.example в .env
и при необходимости заполните ключ. Затем выполните с Node.js 24 и npm:
npm exec --yes [email protected] -- pnpm install --frozen-lockfile
npm exec --yes [email protected] -- pnpm test
npm exec --yes [email protected] -- pnpm build
node --use-system-ca --env-file-if-exists=.env server.ts
19 тестов проверяют фильтры, границу бюджета, редкие и пустые категории, null-часов,
смену даты и декабрьскую занятость, повторяемость, ошибки модели, статические файлы и лимит запросов.
Они используют имитацию OpenAI и не расходуют кредиты.
Для разработки интерфейса npm run dev запускает Vite с прокси /api к серверу на порту 3000.
Docker и Caddy
При установленном и запущенном Docker Desktop:
docker compose up -d --build
Откройте http://localhost:8080. Caddy перенаправляет запросы в соседний контейнер
приложения. Каталог смонтирован только для чтения. Ключ остаётся на сервере;
.env исключён из Git и контекста сборки Docker.
docker compose logs -f
docker compose down
Локально используется HTTP, порт доступен только с этого компьютера. Публичный HTTPS потребует домена/VPS и настройки Caddy на домен с портами 80/443. Текущий запуск предназначен для локального демо. PostgreSQL и регистрация не нужны для задачи рекомендации по неизменяемому каталогу. Живое демо проверено с локальным Node.js; запуск Docker в этой среде не проверялся.
Известные ограничения
- Каталог фиксированный: 66 записей, календарь только с 23 сентября по 31 декабря 2026 года. Нет синхронизации с реальной занятостью подрядчиков.
- Запрос проверяет одну дату; поиск по диапазону дат не реализован.
- Цены стартовые, допускается превышение бюджета до 10%. Итоговая стоимость и бронирование не подтверждаются; оплаты и отправки заявок нет.
- Подбор использует правила и текстовые признаки. ИИ объясняет выбранные профили, но не меняет их состав. Текст ИИ может различаться и требует проверки фактов.
- Карта содержит демонстрационные маркеры, а не актуальную выдачу поиска. Изображения — ИИ-иллюстрации, а не фотографии реальных подрядчиков.
- При сбое OpenAI включается локальное объяснение. Общий лимит процесса — 20 поисков в минуту; он не разделён по пользователям и не рассчитан на высокую нагрузку.
Развёрнутая версия
Публичная ссылка на приложение пока не опубликована. Текущая версия демонстрируется локально: Node.js — localhost:3000 или Docker + Caddy — localhost:8080. Эти адреса открываются только на компьютере, где запущен проект. Для публичного демо необходим отдельный деплой; ссылки на GitHub ведут к исходному коду.
Документация: OpenAI Structured Outputs, Caddy Automatic HTTPS.
Авторы коммитов
История коммитов
- 2026-09-23 17:38Merge pull request #1 from BAITC-Hacks/codex/jury-ready-mainZhan
- 2026-09-23 17:30Merge complete hackathon README into submission branchQadam Developer
- 2026-09-23 17:29docs: complete README for hackathon submissionQadam Developer
- 2026-09-23 17:16Merge verified system certificate fix for jury demoQadam Developer
- 2026-09-23 17:15fix: use system certificates for local OpenAI requestsQadam Developer
- 2026-09-23 17:01Prepare verified HackAlem implementation for mainQadam Developer
- 2026-09-23 17:00test: rehearse truthful December refusal for jury demoQadam Developer
- 2026-09-23 16:45style: use supplied HackAlem logo and faviconQadam Developer
Все коммиты (16)
- 2026-09-23 16:43feat: add generated portraits to contractor cards and profilesQadam Developer
- 2026-09-23 16:35style: remove homepage hero and lead with searchQadam Developer
- 2026-09-23 16:30style: add blue pixel background and matching accentsQadam Developer
- 2026-09-23 16:26feat: redesign homepage with warm green event discovery styleQadam Developer
- 2026-09-23 15:57feat: open contractor details from search and map cardsQadam Developer
- 2026-09-23 15:38Update project implementation and configurationQadam Developer
- 2026-09-23 14:56Migrate matching service to OpenAI and serve secured frontendQadam Developer
- 2026-09-23 14:41feat: добавить поиск подрядчиков с картой Казахстана и подбором через GeminiQadam Developer
Структура репозитория (47 файлов)
.dockerignore .env.example .gitignore .npmrc Caddyfile Dockerfile README.md compose.yaml data/contractors.json data/hackathon-contractors.csv index.html package.json pnpm-lock.yaml scripts/import-contractors.mjs scripts/start.ps1 server.ts src/assets/README.md src/assets/blue-pixels.svg src/assets/contractors/README.md src/assets/contractors/florist.png src/assets/contractors/host-01.png src/assets/contractors/host-02.png src/assets/contractors/host-03.png src/assets/contractors/musician.png src/assets/contractors/photographer.png src/assets/contractors/prompts-hosts.md src/assets/contractors/prompts-performers.md src/assets/contractors/prompts-specialists.md src/assets/event-atmosphere.png src/assets/hackalem-logo.png src/components/common/ContractorDetails.css src/components/common/ContractorDetails.tsx src/components/common/KazakhstanMap.css src/components/common/KazakhstanMap.tsx src/components/home/HomeScreen.css src/components/home/HomeScreen.tsx src/data/catalog-options.json src/data/contractor-images.ts src/main.tsx src/server/matching.ts src/styles.css src/types.ts src/vite-env.d.ts start.cmd tests/server.test.ts tsconfig.json vite.config.ts
Ветки (3)
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.