AAYMA
RU
Войти

HackAlem AI #79-lite

API и веб-интерфейс для подбора до 3 event-подрядчиков из датасета на 66 профилей по городу, дате, формату, категории, бюджету, языку и…

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

О проекте

Команда: Где мой API
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP

Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор

Суть

API и веб-интерфейс для подбора до 3 event-подрядчиков из датасета на 66 профилей по городу, дате, формату, категории, бюджету, языку и длительности, с объяснением выбора и прозрачными альтернативами.

Как устроено

Next.js-форма или свободный текст (intake-парсер) через proxy уходят в NestJS; DTO валидируются, CSV-профили нормализуются. Модуль matching применяет жёсткие фильтры (город, категория, занятость на дату, бюджет, формат, язык, длительность). Допустимые профили передаются в OpenAI Responses API для смыслового ранжирования и объяснений с проверкой через Zod Structured Outputs; при отсутствии ключа или невалидном ответе работает детерминированный fallback. Возвращаются максимум 3 карточки с таблицей сверки и явной причиной, если точных совпадений меньше трёх (matched / no_category_in_city / no_candidates_after_filters).

Ключевые решения

  • hard-constraint filtering
  • LLM reranking
  • Structured Outputs with Zod
  • deterministic fallback scoring
  • transparent alternatives with differences
  • result caching

Особенности

Три явно различимых исхода подбора и альтернативы, у которых перечислены отличия от запроса (дата/бюджет/язык/длительность), вместо молчаливой подмены. Учитываются флаги качества данных (synthetic, city_imputed, price_imputed). Дополнительно сделан Telegram-бот.

Стек

Node.js 22 · TypeScript · NestJS · Next.js 15 · React 19 · Tailwind CSS · shadcn/ui · TanStack Query · Zod · Jest · Vitest · Playwright · Docker Compose · Python (Telegram bot)

Модели

gpt-4o-mini


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-ea63b56a-api.

README

README.md

HackAlem AI #79-lite

API и веб-интерфейс для хакатон-задачи «умный подбор подрядчиков». Проект помогает организатору мероприятия быстро подобрать event-подрядчиков по городу, дате, формату, категории, бюджету, языку и длительности. На выходе пользователь получает до трёх карточек с конкретным объяснением, почему подрядчик подходит или почему точных вариантов меньше трёх.

Публичной deployed-версии пока нет. Проект запускается локально или через Docker Compose.

Что реализовано

  • Подбор подрядчиков из анонимизированного CSV-датасета на 66 профилей.
  • Жёсткие фильтры по городу, категории, занятости на дату, бюджету, формату, языку и длительности.
  • Поддержка составных категорий в CSV через |: запрос по одной категории находит часть составной.
  • Стабильный порядок карточек для одинакового запроса.
  • Максимум 3 карточки в ответе.
  • Три различимых исхода для пользователя:
    • matched — найдены точные или близкие карточки;
    • no_category_in_city — в выбранном городе нет такой категории;
    • no_candidates_after_filters — категория есть, но условия отсеяли кандидатов.
  • Прозрачные альтернативы, если точных совпадений меньше трёх: карточка сохраняет город, категорию и формат, а отличия по дате, бюджету, языку или длительности перечисляются в differences.
  • Персональные объяснения по бюджету, формату, языку, длительности, дате и смыслу описания.
  • Таблица сверки в каждой карточке: запрос заказчика сравнивается с данными исполнителя, а отличия подсвечиваются отдельно и не заменяют человеческое объяснение.
  • Шесть быстрых сценариев с точными результатами, редкой категорией и прозрачными альтернативами.
  • Fallback без OpenAI API: сервис продолжает работать детерминированно без ключа.
  • Веб-интерфейс с ручной формой, вкладкой свободного текста и страницей просмотра датасета.
  • Отображение флагов качества данных: synthetic, city_imputed, price_imputed.
  • Swagger-документация backend API.

Бронирования, платежей, уведомлений и личных кабинетов в текущей версии нет.

Как работает решение

  1. Пользователь открывает Next.js-интерфейс и вводит параметры мероприятия вручную или свободным текстом.
  2. Frontend отправляет запрос через same-origin proxy на NestJS backend.
  3. Backend валидирует DTO и нормализует входные значения.
  4. Модуль contractors читает нормализованные профили из CSV.
  5. Модуль matching применяет обязательные фильтры: город, категория, дата занятости, бюджет, формат, язык и длительность.
  6. Если есть подходящие кандидаты и настроен OPENAI_API_KEY, backend передаёт только допустимые профили в OpenAI Responses API для смыслового ранжирования и генерации объяснений.
  7. Ответ модели проверяется через Zod Structured Outputs и сверяется с исходными профилями.
  8. При отсутствии ключа, таймауте или невалидном ответе включается детерминированный fallback.
  9. Сервис сортирует результат, возвращает максимум три карточки и явно сообщает, почему карточек меньше трёх, если это произошло.

ИИ не имеет права возвращать занятого подрядчика, менять цену или придумывать отсутствующие поля: жёсткие бизнес-правила проверяются обычным кодом до и после вызова модели.

Технологии

Backend:

  • Node.js 22, TypeScript, NestJS;
  • REST API с префиксом /api/v1;
  • @nestjs/config, class-validator, class-transformer;
  • @nestjs/swagger для OpenAPI/Swagger;
  • csv-parse для чтения датасета;
  • официальный пакет openai, Responses API и Structured Outputs;
  • Zod для проверки структурированных ответов модели;
  • nestjs-pino, pino-pretty, helmet, CORS, @nestjs/throttler;
  • Jest и Supertest для тестов.

Frontend:

  • Next.js 15 App Router, React 19, TypeScript;
  • Tailwind CSS;
  • shadcn/ui, Radix UI, Lucide React;
  • React Hook Form, Zod, @hookform/resolvers;
  • TanStack Query;
  • Sonner;
  • Vitest, Testing Library и Playwright.

Инфраструктура:

  • Docker Compose для запуска backend и frontend;
  • named volume recommendations для кэша повторяемых результатов;
  • локальные smoke-скрипты в scripts/.

Архитектура проекта

Репозиторий — монорепозиторий с двумя приложениями:

backend/   NestJS API, подбор подрядчиков, фильтры, OpenAI-интеграция, fallback
frontend/  Next.js web-интерфейс, форма, чат-ввод, просмотр датасета
docs/      архитектура и API-контракт
scripts/   smoke-проверки
tests/     дополнительные тестовые артефакты

Основной поток:

Next.js form/chat
  -> /api/v1/* proxy
  -> NestJS DTO validation
  -> CSV repository
  -> matching filters
  -> OpenAI Responses API + Zod validation или fallback scoring
  -> stable sort
  -> response with exact cards and transparent alternatives

Ключ OpenAI хранится только в backend-переменной OPENAI_API_KEY. Frontend не обращается к OpenAI напрямую и не получает секреты. Подробности архитектуры описаны в docs/ARCHITECTURE.md, контракт API — в docs/API-CONTRACT.md.

Основная ветка

main — единственная основная ветка проекта. Актуальная версия решения, изменения для демонстрации и итоговая версия для сдачи хранятся в main; в рамках текущего процесса отдельные рабочие ветки не используются.

Установка и запуск локально

Нужны Node.js 22, npm и два терминала. Команды выполняются из корня репозитория.

Backend:

npm --prefix backend ci
npm --prefix backend run start:dev

Frontend:

npm --prefix frontend ci
npm --prefix frontend run dev

После запуска:

Без API-ключа приложение работает в fallback-режиме. Для включения ИИ создайте backend/.env по примеру backend/.env.example и задайте OPENAI_API_KEY:

PORT=3001
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o-mini
OPENAI_TIMEOUT_MS=7000
DATASET_PATH=./data/contractors.csv
CACHE_DIR=./cache
FRONTEND_ORIGIN=http://localhost:3000

Frontend использует серверную переменную BACKEND_URL из frontend/.env.example. Не кладите API-ключ в NEXT_PUBLIC_*.

Запуск через Docker

Docker Desktop должен быть запущен в режиме Linux containers.

docker compose config --quiet
docker compose build
docker compose up -d --wait
node scripts/smoke.mjs

Открыть интерфейс: http://localhost:3000. Swagger: http://localhost:3001/api/docs.

backend/.env подключается при наличии, но запуск без него поддерживается. В Docker frontend обращается к backend по http://backend:3001, а исходный CSV копируется в backend-образ без изменения. Кэш рекомендаций хранится в named volume recommendations, поэтому переживает restart/rebuild.

Для остановки:

docker compose down

Для других портов используйте FRONTEND_PORT и BACKEND_PORT; для smoke-проверки — FRONTEND_URL и API_URL.

Как проверить решение

Самый простой повторяемый сценарий для жюри:

  1. Запустить проект через Docker Compose.
  2. Открыть http://localhost:3000.
  3. Выбрать демо-сценарий или вручную ввести:
    • город: Алматы;
    • дата: 2026-10-15;
    • формат: корпоратив;
    • категория: Ведущий;
    • бюджет: 900000;
    • язык: русский.
  4. Убедиться, что возвращаются до трёх карточек, у каждой есть конкретное объяснение и таблица критериев.
  5. Повторить запрос с датой 2026-12-20: выдача должна измениться из-за декабрьской занятости.
  6. Проверить редкий сценарий: Астана, 2026-11-14, Флорист, бюджет 900000.
  7. Проверить пустой сценарий: плотный запрос с бюджетом 1 или Астана + Инструменталист.

Быстрые сценарии на главной странице подобраны по текущему датасету и всегда показывают рекомендации:

СценарийЧто демонстрируетОжидаемый результат
Корпоратив · ведущийПлотная точная выдача в Алматы3 точных
Корпоратив · флористРедкая категория в Астане1 точный
Корпоратив · новая датаЗанятость на выбранную дату3 близкие альтернативы
Свадьба · фотографДлительность работы 6 часов3 точных
Той · ведущийКазахский язык и длительность2 точных + 1 альтернатива
Свадьба · банкетный залКомбинированная выдача1 точный + 2 альтернативы

Нулевая выдача остаётся частью корректного поведения: система не подменяет категорию и не рекомендует профиль, если безопасной альтернативы в заданных пределах нет.

Те же проверки можно выполнить автоматически:

node scripts/smoke.mjs

Smoke проверяет healthcheck, catalog, intake parser, рекомендации, альтернативы, повторяемость, датасет, валидацию, frontend и proxy.

Пример прямого API-запроса:

curl -s http://localhost:3001/api/v1/recommendations \
  -H 'Content-Type: application/json' \
  -d '{"city":"Алматы","date":"2026-10-15","eventFormat":"корпоратив","category":"Ведущий","budgetKzt":900000,"language":"русский"}'

Браузерная проверка всей связки:

npm --prefix frontend exec -- playwright install chromium
node scripts/browser-smoke.mjs

Проверки качества:

npm --prefix backend run lint
npm --prefix backend test
npm --prefix backend run build
npm --prefix frontend run lint
npm --prefix frontend test
npm --prefix frontend run build

Данные и интеграции

Используемые данные:

  • hackathon dataset anonymized.csv — основной источник профилей подрядчиков;
  • HackAlem AI_ Хакатон-задача_ умный подбор подрядчиков.docx — исходное ТЗ;
  • hackathon dataset preview.html — локальный preview исходных данных.

Характеристики датасета:

  • 66 строк, 13 колонок;
  • города: Алматы — 50, Астана — 15, Зарубежье — 1;
  • synthetic — 13 профилей;
  • city_imputed — 8 профилей;
  • price_imputed — 18 профилей;
  • max_hours = null означает, что работа не привязана к присутствию на площадке.

Внешние сервисы:

  • OpenAI API используется опционально для смыслового ранжирования и генерации объяснений;
  • без OpenAI API работает локальный детерминированный fallback;
  • других внешних сервисов, баз данных, очередей или векторных хранилищ в MVP нет.

Ограничения текущей версии

  • Публичной deployed-версии пока нет.
  • Поддерживается MVP-топология с одним backend-процессом.
  • База данных и Redis не используются; CSV остаётся единственным источником профилей.
  • Календарь доступности ограничен диапазоном 2026-09-23 — 2026-12-31; даты вне окна отклоняются с HTTP 400.
  • Результаты кэшируются как JSON-снимки. Изменение CSV, модели, версии промпта/пайплайна или удаление кэша создаёт новый набор результатов.
  • Первый сохранённый результат для запроса может быть fallback, если в момент первого запроса OpenAI был недоступен.
  • Нет дообучения модели на 66 строках и нет embeddings/vector search.
  • Нет реального бронирования подрядчиков, оплаты, календарной синхронизации и уведомлений.
  • Синтетические профили допустимы только с флагом synthetic: true; интерфейс показывает этот флаг пользователю.

Полезные ссылки

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

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

Matrasos 14risest 8Kerajo 6

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

  • 2026-09-23 17:52update: READMEMatrasos
  • 2026-09-23 17:52update: UIMatrasos
  • 2026-09-23 17:47test: verify seasonal availability for dense demorisest
  • 2026-09-23 17:47update: README testsMatrasos
  • 2026-09-23 17:47feat: add Python Telegram bot for contractor recommendationsKerajo
  • 2026-09-23 17:43updateMatrasos
  • 2026-09-23 17:39Merge branch 'main' of https://github.com/BAITC-Hacks/hack-ea63b56a-apiMatrasos
  • 2026-09-23 17:39updateMatrasos
Все коммиты (28)
  • 2026-09-23 17:31test: add application audit scenarios and reportsrisest
  • 2026-09-23 17:31test: add recommendation scenarios and requirements auditrisest
  • 2026-09-23 17:30updateMatrasos
  • 2026-09-23 17:20add: features and UIMatrasos
  • 2026-09-23 17:03docs: add design reference for form-first redesignKerajo
  • 2026-09-23 16:45feat: improve grounded contractor explanations and result messagesKerajo
  • 2026-09-23 16:45update: UIMatrasos
  • 2026-09-23 16:38test: reject empty matching field values in datasetrisest
  • 2026-09-23 16:35feat: redesign recommender with shadcnMatrasos
  • 2026-09-23 16:31test: add root matching scenarios and full verification reportrisest
  • 2026-09-23 16:14feat: add OpenAI integration and harden recommendationsKerajo
  • 2026-09-23 15:46feat: complete AI contractor recommenderMatrasos
  • 2026-09-23 15:35test: validate dataset prices and hour limitsrisest
  • 2026-09-23 15:26feat: add contractor recommendation appKerajo
  • 2026-09-23 15:22test: validate contractor quality flagsrisest
  • 2026-09-23 14:58add: back & frontMatrasos
  • 2026-09-23 14:31test: add two dataset integrity checksrisest
  • 2026-09-23 14:15chore: test commitKerajo
  • 2026-09-23 13:44Configure hackathon contractor recommender projectMatrasos
  • 2026-09-23 13:37Add Codex wrapperMatrasos
Структура репозитория (127 файлов)
.21st/DESIGN.md
.21st/design.json
.codex/config.toml
.codex/rules/core.md
.codex/rules/memory.md
.codex/rules/nest.md
.dockerignore
.gitignore
AGENTS.md
HackAlem AI_ Хакатон-задача_ умный подбор подрядчиков.docx
README.md
backend/.dockerignore
backend/.env.example
backend/.gitignore
backend/.gitkeep
backend/Dockerfile
backend/Dockerfile.dockerignore
backend/README.md
backend/data/contractors.csv
backend/eslint.config.mjs
backend/jest.config.cjs
backend/nest-cli.json
backend/package-lock.json
backend/package.json
backend/src/ai/ai.module.ts
backend/src/ai/ai.service.ts
backend/src/app.module.ts
backend/src/common/configure-app.ts
backend/src/common/domain.ts
backend/src/common/evidence.ts
backend/src/config/environment.ts
backend/src/config/runtime-config.module.ts
backend/src/contractors/contractors.controller.ts
backend/src/contractors/contractors.dto.ts
backend/src/contractors/contractors.module.ts
backend/src/contractors/contractors.service.ts
backend/src/contractors/csv-contractors.loader.ts
backend/src/health/health.controller.ts
backend/src/health/health.module.ts
backend/src/intake/intake-ai.service.ts
backend/src/intake/intake.controller.ts
backend/src/intake/intake.dto.ts
backend/src/intake/intake.module.ts
backend/src/intake/intake.service.ts
backend/src/main.ts
backend/src/matching/matching.module.ts
backend/src/matching/matching.service.ts
backend/src/recommendations/explainer.service.ts
backend/src/recommendations/recommendation.dto.ts
backend/src/recommendations/recommendations.controller.ts
backend/src/recommendations/recommendations.module.ts
backend/src/recommendations/recommendations.service.ts
backend/src/recommendations/snapshot.service.ts
backend/test/ai.spec.ts
backend/test/backend.spec.ts
backend/test/config.spec.ts
backend/test/contractors.spec.ts
backend/test/intake.spec.ts
backend/tsconfig.build.json
backend/tsconfig.json
docker-compose.yml
docs/ACCEPTANCE.md
docs/API-CONTRACT.md
docs/ARCHITECTURE.md
frontend/.dockerignore
frontend/.env.example
frontend/.gitignore
frontend/.gitkeep
frontend/Dockerfile
frontend/components.json
frontend/e2e/recommender.spec.ts
frontend/eslint.config.mjs
frontend/next-env.d.ts
frontend/next.config.ts
frontend/package-lock.json
frontend/package.json
frontend/playwright.config.ts
frontend/postcss.config.js
frontend/public/brand-mark.svg
frontend/src/app/api/v1/[...path]/route.ts
frontend/src/app/dataset/api/route.ts
frontend/src/app/dataset/page.tsx
frontend/src/app/globals.css
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/components/app-header.tsx
frontend/src/components/dataset-explorer.test.tsx
frontend/src/components/dataset-explorer.tsx
frontend/src/components/intent-assistant.tsx
frontend/src/components/providers.tsx
frontend/src/components/recommender.test.tsx
frontend/src/components/recommender.tsx
frontend/src/components/ui/alert.tsx
frontend/src/components/ui/badge.tsx
frontend/src/components/ui/button.tsx
frontend/src/components/ui/card.tsx
frontend/src/components/ui/input.tsx
frontend/src/components/ui/label.tsx
frontend/src/components/ui/select.tsx
frontend/src/components/ui/separator.tsx
frontend/src/components/ui/skeleton.tsx
frontend/src/components/ui/table.tsx
frontend/src/components/ui/tabs.tsx
frontend/src/components/ui/textarea.tsx
frontend/src/components/ui/tooltip.tsx
frontend/src/lib/api.test.ts
frontend/src/lib/api.ts
frontend/src/lib/dataset-api.test.ts
frontend/src/lib/dataset-api.ts
frontend/src/lib/form.test.ts
frontend/src/lib/form.ts
frontend/src/lib/utils.ts
frontend/tailwind.config.ts
frontend/tsconfig.json
frontend/vitest.config.ts
frontend/vitest.setup.ts
hackathon dataset anonymized.csv
hackathon dataset preview.html
scripts/browser-smoke.mjs
scripts/smoke.mjs
tests/AI-SCENARIOS-REPORT.md
tests/README.md
tests/REQUIREMENTS-REVIEW.md
tests/TEST-REPORT.md
tests/matching.spec.cjs
tests/recommendation-scenarios.spec.cjs
tests/site-scenarios.mjs
Ветки (5)

Обсуждение 0 комментариев

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

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