QADAM
Next.js-сервис подбора до трёх подрядчиков под мероприятие из датасета на 66 профилей.
О проекте
Команда: QADAM
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Доведённый продукт
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Next.js-сервис подбора до трёх подрядчиков под мероприятие из датасета на 66 профилей. Отбор детерминированный, объяснения даёт LLM, а при сбое модели срабатывает fallback.
Как устроено
Форма отправляет POST /api/recommend, запрос проверяется Zod и нормализуется (NFKC, lower-case). CSV кэшируется на сервере. Сначала работают жёсткие фильтры: город, категория, занятые даты, бюджет, формат, язык, часы. Затем ранжирование score = 30×(budget−price)/budget со стабильным tie-break, в выдачу идут топ-3 ID. LLM (gpt-4o-mini или NVIDIA endpoint) только формулирует объяснения. Ответ валидируется, при ошибке используются детерминированные объяснения. Статусы: matched / no_category_in_city / no_eligible_candidates, плюс диагностика причин исключения.
Ключевые решения
- детерминированные жёсткие фильтры
- ранжирование по запасу бюджета
- Zod-валидация
- LLM-объяснения с fallback
- Playwright e2e
Метрики
6/6 acceptance-сценариев прошли браузерную проверку; live-запрос к gpt-4o-mini: HTTP 200 за 3,86 с
Особенности
LLM не влияет на состав и порядок выдачи, только на текст, поэтому результат воспроизводим. Отсутствие даты в busy_dates не выдаётся за подтверждённую доступность, запросы вне окна календаря отклоняются. Синтетические профили и восстановленные цены помечены.
Стек
Next.js 16 · React 19 · TypeScript · Tailwind CSS · Zod · csv-parse · Vitest · Playwright · Vercel
Модели
OpenAI gpt-4o-mini · NVIDIA API (опционально)
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-5ddfd007-qadam.
README
README.mdQADAM — умный подбор подрядчиков
QADAM — русскоязычный сервис для быстрого и прозрачного подбора подрядчиков на мероприятия, созданный для HackAlem AI 2026, трек 79-lite. Он детерминированно проверяет город, дату, формат, категорию, бюджет, язык и длительность, затем показывает до трёх подходящих профилей с проверяемым объяснением выбора.
Оригинальный анонимизированный набор содержит 66 профилей и 13 колонок. Все шесть acceptance-сценариев прошли автоматизированную браузерную проверку. Приложение работает без credentials: правила сохраняют тот же отбор и формируют безопасные fallback-объяснения; модель влияет только на текст объяснения.
Быстрая демонстрация
- Много вариантов: Алматы, ведущий, корпоратив, 10.10.2026 — три стабильных результата с разными ценами.
- Редкая категория: Алматы, флорист, свадьба, 05.10.2026 — один результат после проверки календаря.
- Нет результата: Астана, декоратор, свадьба, 05.10.2026 — честный статус
no_category_in_cityбез вымышленных рекомендаций.
Стек: Next.js 16, React 19, TypeScript, Tailwind CSS, Zod, csv-parse, Vitest, Playwright, OpenAI gpt-4o-mini с детерминированным fallback.
Самый простой запуск без ключей:
git clone https://github.com/BAITC-Hacks/hack-5ddfd007-qadam
cd hack-5ddfd007-qadam
npm ci
npm run dev
Открыть http://127.0.0.1:3000. Встроенный CSV используется автоматически, а без AI-переменных включаются детерминированные объяснения.
Запуск для жюри
Проверенная среда: Windows PowerShell, Node 24.21.0, npm 11.19.0. Версия Node указана в .nvmrc; диапазон проекта — Node 22–24. Node 22 отдельно не проверялся. Next.js 16.3.6, React 19.3.0, TypeScript 7.0.2, Vitest 5.0.1; все зависимости зафиксированы в package-lock.json.
git clone https://github.com/BAITC-Hacks/hack-5ddfd007-qadam
cd hack-5ddfd007-qadam
npm ci
Copy-Item .env.example .env.local
npm run validate:data
npm test
npm run dev
Открыть http://127.0.0.1:3000. Эти команды запускают приложение без персональных credentials: .env.example оставляет AI_PROVIDER=none, поэтому используются детерминированные объяснения. На macOS/Linux вместо Copy-Item используйте cp .env.example .env.local; остальные команды те же. Тестирование ОС проводилось на Windows.
Сборка и production-запуск:
npm run typecheck
npm run build
npm start
Полная локальная проверка, включая настоящий Chromium:
npm ci
Copy-Item .env.example .env.local
npm run validate:data
npm test
npm run typecheck
npm run build
npx playwright install chromium
npm run test:e2e
Не запускайте dev и production одновременно на одном порту. Для другого порта: npm start -- --port 3001. Сервер по умолчанию слушает только 127.0.0.1. При отсутствии/повреждении CSV страница показывает ошибку настройки, API возвращает 503 — пустые рекомендации не фабрикуются.
Актуальные фактические проверки и checkpoint SHA записаны в docs/PROGRESS.md. Набор данных включён в официальный main начиная с Milestone 4 для независимой оценки; это не означает, что организаторы предоставили отдельную лицензию или разрешение на дальнейшее перераспространение.
Оригинальные данные и права доступа
Источник — оригинальный анонимизированный набор, предоставленный организаторами для задания HackAlem AI 2026. Для независимой оценки его байты сохранены без изменений в data/hackathon-dataset-anonymized.csv. Корневой рабочий файл участника и любые другие CSV по-прежнему исключены из Git.
В официальном репозитории, его issues, истории и доступных track-материалах не найдены лицензия набора или прямое разрешение организаторов на перераспределение. Файл опубликован участником для независимой оценки, но это не заменяет разрешение правообладателя. Перед дальнейшим распространением необходимо отдельно проверить права на данные.
Ожидаемый SHA-256 исходных байтов:
6a724b6b7dfb5973343e68ba18dadb60fc807d87e3d78f03ee86fb26cb089f7d
Набор содержит 66 профилей и 13 колонок:
| Колонка | Значение |
|---|---|
id, anon_name | стабильный анонимный ID и отображаемое анонимизированное имя |
categories, city | категории через | и значение местоположения |
city_imputed, synthetic | город восстановлен; профиль синтетический |
price_from_kzt, price_imputed | стартовая цена в KZT; цена восстановлена |
event_formats, languages | поддерживаемые форматы и языки через | |
max_hours | максимальная длительность либо пусто, когда ограничение неприменимо |
busy_dates | занятые даты через | |
description | недоверенный текст профиля, отображаемый только как экранированный текст |
Есть 17 категорий. Города: Алматы — 50, Астана — 15, Зарубежье — 1. Флаг synthetic=true стоит у 13 профилей: эти записи нельзя представлять как независимо подтверждённых реальных подрядчиков. price_imputed=true у 18 записей и city_imputed=true у 8: восстановленные значения явно помечаются в карточках. В 9 строках max_hours пуст, что означает «ограничение не применяется», а не ноль. Значение «Зарубежье» сохранено как значение исходного каталога и не трактуется как конкретный город Казахстана.
Календарь покрывает только 2026-09-23…2026-12-31 включительно. Запросы вне окна отклоняются: отсутствие данных не считается доступностью. Профиль исключается, когда дата присутствует в busy_dates, включая площадки и банкетные залы. Отсутствие даты в списке означает лишь отсутствие отметки о занятости в этом статическом наборе, не подтверждение бронирования и не актуальную доступность.
csv-parse/sync обрабатывает RFC4180, запятые и переводы строк внутри кавычек. Списки разделены |; флаги True/False нечувствительны к регистру; пустые часы → null. Zod проверяет каждую запись, дубли ID, поля и даты; ошибки содержат номер логической CSV-записи и поле. Проверяются сводные количества и SHA-256. Скрипт выводит точные количества по категориям.
Для другого расположения укажите в .env.local:
DATASET_PATH=C:/path/to/hackathon-dataset-anonymized.csv
Next.js и команды проверки читают .env.local. Без переопределения используется data/hackathon-dataset-anonymized.csv. outputFileTracingIncludes добавляет этот точный файл в server traces главной страницы и /api/recommend; production-сервер читает его по DATASET_PATH. CSV остаётся серверным файлом и не публикуется как статический URL.
Архитектура и точный алгоритм
Russian React form
→ POST /api/recommend (Next.js Node route handler)
→ ограничение тела 4096 байт + Zod + нормализация
→ серверный CSV-кэш (путь, размер, mtime, ctime)
→ город + категория
→ календарь + цена + формат + язык + часы
→ стабильное ранжирование → максимум 3 ID
→ факты + объяснения по правилам
→ [опционально] один AI-запрос → валидация / fallback
→ карточки + диагностика + источник объяснения
Функции в lib/matching.ts чистые и тестируются без Next.js и ключа. Совпадения строк нормализуются через NFKC, trim, схлопывание пробелов и lower-case ru. Опции формы строятся из фактических полей CSV. Сервер канонизирует выбранные опции, ограничивает строки, числовой бюджет (1…1 000 000 000 ₸), целые часы (1…168) и календарную дату.
Жёсткие ограничения одинаковы для всех, включая площадки и банкетные залы. Дата в busy_dates всегда исключает профиль. Стартовая цена не выше бюджета. Формат и язык должны быть явно перечислены. Часы исключают профиль только при ненулевом лимите и превышении этого лимита.
Рейтинг: score = 30 × (budgetKzt − priceFromKzt) / budgetKzt, затем score DESC, priceFromKzt ASC, id ASC с явным сравнением строк. Внутри одного запроса это предпочтение большего запаса бюджета, а не заявление о качестве. Описания, случайность, текущее время и LLM не влияют на порядок. Одинаковые нормализованные запросы дают одинаковые ID в том же порядке.
| Статус | Значение |
|---|---|
matched | Есть хотя бы один подходящий профиль, показано 1–3 |
no_category_in_city | В городе нет ни одного профиля категории |
no_eligible_candidates | Категория есть, но все профили исключены ограничениями |
Диагностика рассчитывается только среди профилей выбранного города и категории. excludedByReason учитывает все причины, поэтому числа могут пересекаться. primaryExclusion использует порядок busy_date → over_budget → format_mismatch → language_mismatch → duration_exceeded и даёт непересекающиеся количества. eligibleBeforeTop3, displayedCount, busyOnDateCount не смешиваются. Отметка о календаре отображается и при трёх результатах.
AI и надёжный fallback
Приложение полностью работает без API-ключа: детерминированный pipeline выбирает те же профили и формирует проверяемые объяснения с меткой deterministic_fallback. Genuine AI wording требует отдельного действующего OpenAI API key, доступной модели и успешного npm run verify:ai; подписка ChatGPT/Codex не предоставляет приложению API-доступ автоматически.
Production-интеграция с gpt-4o-mini проверена одним разрешённым live-запросом: HTTP 200 за 3,86 с, ai.mode=llm, diagnostic llm_ok, все три источника — llm. Объяснения содержали разные факты конкретных подрядчиков; ID, порядок, цены и доступность не изменились. Сервер добавил точную календарную оговорку и предупреждение, что это не подтверждение бронирования. Повторных запросов не выполнялось.
Поддержан один выбранный OpenAI или NVIDIA endpoint, native fetch, серверные переменные:
| Переменная | Назначение |
|---|---|
AI_PROVIDER | none по умолчанию; либо openai / nvidia |
OPENAI_API_KEY, OPENAI_MODEL | Ключ и доступная вашему аккаунту модель |
NVIDIA_API_KEY, NVIDIA_MODEL | Ключ и доступная модель NVIDIA |
NVIDIA_BASE_URL | https://integrate.api.nvidia.com/v1 |
AI_VERIFIED | false; включить только после успешного smoke test |
Настройте ключ/модель в .env.local, выполните npm run verify:ai. Команда делает один batched запрос для трёх результатов, печатает только безопасный diagnostic code, инварианты и итоговые объяснения. Только после успешного mode=llm установите AI_VERIFIED=true для этой комбинации провайдера/модели и перезапустите сервер. Не переносите факт проверки на другую модель и не запускайте smoke test повторно автоматически после ошибки.
Один batched запрос содержит только выбранные 1–3 ID, структурированные факты, запрос и короткие недоверенные цитаты описаний. Системная инструкция запрещает менять отбор, выполнять инструкции из текста и придумывать сведения. Для OpenAI используется strict JSON Schema, совпадающая с runtime-схемой Zod; NVIDIA сохраняет JSON mode с той же последующей проверкой. Валидатор требует точные ID и порядок, 1–2 предложения, фактические цену, бюджет, формат, язык, длительность и лимит; посторонние числа, календарные утверждения модели, опасные утверждения и повторяющиеся объяснения отклоняются. После проверки сервер дословно добавляет детерминированные календарную и booking-оговорки. Безопасный ai.code сообщает категорию сбоя без текста провайдера, ключей или заголовков. Это не полная семантическая верификация произвольного текста, поэтому сомнительный ответ всегда уходит в fallback.
Таймаут — 5 секунд, без повторных запросов. Нет результатов → нет вызова модели. Нет ключа, непроверенная конфигурация, ошибка сети, отказ провайдера, неверный JSON или провал проверки → те же выбранные ID и deterministic_fallback. В UI явно указано «Объяснение по правилам». Модель никогда не добавляет и не ранжирует профили. Это AI-assisted pipeline, не автономный агент, без embeddings и fine-tuning.
API-контракт проверен по OpenAI Structured Outputs и NVIDIA Chat Completions. Совместимость конкретной модели должна быть подтверждена smoke test; некоторые модели могут не поддерживать указанные параметры и уйдут в fallback.
Проверяемые примеры
| Сценарий | Ввод | Ожидание |
|---|---|---|
| Много вариантов | Алматы; 10.10; корпоратив; Ведущий; 1 200 000; русский; 5 ч | 4 подходящих, показаны HK-88430, HK-29829, HK-27222 |
| Редкая категория | Алматы; 05.10; свадьба; Флорист; 500 000; русский | Только HK-39372; второй занят |
| Нет категории | Астана; 05.10; свадьба; Декоратор; 1 000 000 | no_category_in_city |
| Занятый зал | Астана; 14.11; свадьба; Банкетный зал; 3 000 000 | no_eligible_candidates; HK-90012 занят |
| Фотографы A | Алматы; 10.10; свадьба; Фотограф; 1 500 000 | Множество HK-30583, HK-53108, HK-16628 |
| Фотографы B | То же, 11.10 | Множество HK-76268, HK-68220 |
Все даты — 2026 год. Первые три случая доступны кнопками, фотографы и занятый зал — вторичными действиями под формой. При первом открытии показан детерминированный пример «Много вариантов» с теми же значениями в форме. Смена полей помечает предыдущий результат как устаревший до отправки.
npm test
npm run validate:data
npm run typecheck
npm run build
npx playwright install chromium
npm run test:e2e
Playwright запускает настоящий Chromium против production-сервера. Все шесть acceptance-сценариев прошли; также проверены повторный порядок, мобильная форма 390px, ошибка даты, сетевой отказ и повтор, отсутствие горизонтального переполнения и отсутствие публичного доступа к CSV/.env.local. Axe-core выполняет автоматизированный WCAG A/AA аудит desktop/mobile. Автоматизированный аудит не заменяет ручную проверку доступности. Скриншоты: artifacts/desktop-dense.png, desktop-sparse.png, desktop-empty.png, desktop-date-change.png, mobile-dense.png. Они локальные, генерируются заново, не включены в Git. HTML-отчёт — playwright-report/index.html; трассы сбоя — test-results/.
Реально исполненные результаты и известные ограничения фиксируются в PROGRESS, сценарий выступления — DEMO. Автоматизированная проверка браузера не равна ручному подтверждению участником.
API
POST /api/recommend
Content-Type: application/json
{
"city": "Алматы",
"eventDate": "2026-10-10",
"eventFormat": "корпоратив",
"category": "Ведущий",
"budgetKzt": 1200000,
"language": "русский",
"durationHours": 5
}
Ответ содержит status, message, results (до трёх), diagnostics, ai. Карточка включает ID, анонимизированное имя, город, категорию, стартовую цену, флаги, объяснение, explanationSource и ограниченные факты evidence. Полный CSV/полные профили не отдаются. Опции передаются сервером странице, отдельный API списка не нужен.
Ошибки: 400 — неверные поля/дата/JSON, 413 — тело больше 4096 байт, 415 — не JSON, 503 — недоступен исходный каталог. Клиент ограничивает ожидание 12 сек, отключает повторную отправку, показывает ошибки и кнопку повтора.
Ограничения, безопасность и лицензии
- Цены от, не финальные сметы. Отсутствие занятой даты — лишь запись в календаре, не подтверждение брони. Вне известного окна запрос отклоняется.
- Синтетические профили и восстановленные цена/город явно помечаются. Описания — утверждения исходного профиля, не независимая проверка. React отображает текст с экранированием, без
dangerouslySetInnerHTML. .env.local, все CSV кроме точного путиdata/hackathon-dataset-anonymized.csv, сборки, отчёты и screenshots игнорируются Git. Секреты доступны только серверу; ошибки провайдера и стеки не возвращаются клиенту. Рекомендуемый локальный запуск не использует внешние шрифты, аналитику или изображения. При включении AI факты выбранных профилей отправляются выбранному провайдеру.- Production-развёртывание доступно на hack-5ddfd007-qadam.vercel.app. CSV и
.env.localнедоступны по публичным URL; для платного API при дальнейшем публичном использовании рекомендуется защита от злоупотреблений. - Проект создан с помощью Codex в рабочем окне 23.09.2026; реальные checkpoint-времена и SHA указаны в PROGRESS. Обученные модели, embeddings и шаблоны сайта не использовались. Иллюстрация в UI — CSS/SVG, без внешних assets; live Production AI верифицирован ровно одним финальным запросом.
- Исходный репозиторий не содержал лицензии приложения; лицензия организаторских данных неизвестна. Зависимости сохраняют свои лицензии: Next.js, React, Tailwind, csv-parse, Zod, Vitest, tsx — MIT; TypeScript и Playwright — Apache-2.0;
server-only— MIT. Точные тексты см. в установленных пакетах и lockfile. - При использовании live API действуют условия OpenAI или NVIDIA API Catalog. Квоты, стоимость, модель и разрешение на передачу данных должны соответствовать вашему аккаунту; приложение их не обещает.
Последующие push, PR, merge и deployment выполняются только после отдельного явного разрешения участника. Наличие публичного репозитория и deployment само по себе не подтверждает выполнение формальной процедуры сдачи организаторам.
Future Improvements — не реализовано
Для значительно большего каталога можно добавить PostgreSQL: нормализованные таблицы профилей и календарей, индексы по городу/категории/цене/датам и транзакционное обновление доступности. Это только предложение для будущего масштаба. Текущая реализация не использует PostgreSQL, миграции, ORM или внешний database service; она намеренно читает и кэширует проверенный 66-строчный CSV на сервере.
Авторы коммитов
История коммитов
- 2026-09-23 17:27Finalize README for HackAlem submissioniless
- 2026-09-23 17:06Redesign QADAM with premium dark themeiless
- 2026-09-23 16:46fix: append deterministic availability disclaimersiless
- 2026-09-23 16:06chore: include dataset in deployment traceiless
- 2026-09-23 15:36chore: prepare independent evaluation datasetiless
- 2026-09-23 15:03fix: harden AI explanation validationiless
- 2026-09-23 14:15feat: milestone 2 Russian UI and verified accessible user flowiless
- 2026-09-23 13:57feat: checkpoint deterministic contractor matching and validationiless
Структура репозитория (41 файлов)
.env.example .gitattributes .gitignore .nvmrc README.md app/api/recommend/route.ts app/globals.css app/layout.tsx app/page.tsx components/ContractorCard.tsx components/Icons.tsx components/MatchingApp.tsx components/RequestForm.tsx components/ResultsPanel.tsx data/hackathon-dataset-anonymized.csv docs/DEMO.md docs/PROGRESS.md lib/api.ts lib/data.ts lib/dataset.ts lib/demo-cases.ts lib/evidence.ts lib/explain.ts lib/matching.ts lib/schema.ts lib/types.ts next-env.d.ts next.config.ts package-lock.json package.json playwright.config.ts postcss.config.mjs scripts/validate-dataset.ts scripts/verify-ai.ts tests/api.test.ts tests/browser/matching.spec.ts tests/dataset.test.ts tests/explain.test.ts tests/matching.test.ts tsconfig.json vitest.config.ts
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.