AAYMA
RU
Войти

HACKALEM AI — умный подбор подрядчиков

Веб-приложение, которое по городу, дате, категории, формату и бюджету мероприятия подбирает до трёх подрядчиков из каталога (66 профилей), объясняет…

Рабочий MVP
Команда «BELES» 7💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: 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.md

HACKALEM 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

  1. В корне проекта скопируйте .env.example в .env.
  2. Для ИИ заполните OPENAI_API_KEY в .env. Для локального режима оставьте поле пустым.
  3. Дважды нажмите start.cmd.
  4. Дождитесь сообщения Open http://localhost:3000 и откройте этот адрес.
  5. Выберите пример запроса или заполните форму и нажмите «Найти подрядчиков».
  6. Окно запуска оставьте открытым. Для остановки — 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 в текущем коде не используются.

Как работает подбор

  1. Сервер проверяет обязательные город, дату, формат, категорию и числовой бюджет. Язык и длительность необязательны.
  2. Находит категорию в выбранном городе. Если профилей нет — исход no_category.
  3. Исключает занятых на дату, не поддерживающих формат/язык/длительность и тех, чья стартовая цена выше бюджета более чем на 10%. Допуск сохранён по условиям проекта; превышение бюджета обязательно отмечается в объяснении.
  4. Если никто не прошёл, возвращает no_match с причинами и числом исключений. Один профиль может не пройти несколько условий, поэтому счётчики причин не суммируются.
  5. Ранжирует детерминированно: в пределах бюджета — 100 баллов; каждое совпадение тематического корня формата в описании — 10; упоминание запрошенного языка — 5. При равенстве — меньшая цена, затем ID. Это простая объяснимая эвристика, не эмбеддинги.
  6. Берёт топ-5 для контекста, фиксирует первые 3 на сервере и просит OpenAI написать 1–2 конкретных предложения для каждого выбранного ID. Модель не меняет состав и порядок.
  7. Проверяет ID, повторы и пустые/шаблонные объяснения. Возвращает исходные объекты подрядчиков; ответ модели не может подменить цену или календарь.

При matched показывается до трёх карточек: имя, категории, город, цена, происхождение профиля и объяснение. Если подходящих меньше трёх — показываются все и причина. Пустые исходы объясняются сервером без обращения к ИИ; HTTP 200 отличает их от технических ошибок.

temperature: 0 используется для объяснений, но гарантия порядка обеспечивается серверным ранжированием, а не этой настройкой. Для одной версии каталога одинаковый запрос даёт одинаковые ID в том же порядке, даже если ИИ вернул объяснения в другом порядке.

Вызов ИИ ограничен 7,5 секундами и 1200 выходными токенами, store: false. При отсутствии ключа, ошибке API или плохом ответе используются локальные объяснения: проверенные условия плюс конкретная цитата из description. Источник виден пользователю. Это позволяет сохранить рабочий подбор при недоступности ИИ; реальную задержку API нужно измерять со своим ключом. Общий лимит сервера — 20 поисков в минуту.

Демонстрация для жюри

Во всех примерах язык — русский, длительность не задана.

Для трёх основных сценариев нажмите соответствующую карточку в блоке «Можно начать с идеи», затем «Найти подрядчиков». При рабочем OpenAI в карточках появляется «Объяснение ИИ»; «По данным каталога» означает локальный режим.

СценарийГород / категорияФорматДатаБюджетОжидаемый результат
Плотная категорияАлматы / Ведущийкорпоратив06.10.20261 000 000 ₸3 из 6 подходящих
Изменение датыАлматы / Ведущийкорпоратив07.10.20261 000 000 ₸3, состав меняется из-за календаря
Редкая категорияАлматы / Флористсвадьба23.09.2026300 000 ₸2, объяснение почему меньше трёх
Все занятыАлматы / Флористсвадьба25.12.2026300 000 ₸no_match, оба заняты
Нет категорииЗарубежье / Ведущийкорпоратив06.10.20263 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.

Звёзды
⭐ 0
Форки
0
Язык
TypeScript
Последний push
в прошлом месяце
Коммиты
16
Авторы
1
Файлы
47
Ветки
3

Авторы коммитов

Zhan 16

История коммитов

  • 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 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.