EventDNA
Веб-приложение детерминированного подбора до трёх event-подрядчиков из каталога 66 профилей с объяснениями WHY THIS / WHY NOT, воронкой отбора и API…
О проекте
Команда: stackattack
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Веб-приложение детерминированного подбора до трёх event-подрядчиков из каталога 66 профилей с объяснениями WHY THIS / WHY NOT, воронкой отбора и API сценариев WHAT IF.
Как устроено
FastAPI загружает CSV, последовательно применяет фильтры (категория → город → формат → занятые даты → бюджет → длительность → язык), для каждого исключённого сохраняет первую нарушенную причину. Ранжирование — индекс запаса бюджета (budget − price)/budget с tie-break по ID. /api/what-if пересчитывает соседние сценарии (бюджет +50–200k, дата +1–3 дня, длительность −1–2 ч) и сравнивает появившихся/исчезнувших кандидатов. LLM не используются.
Ключевые решения
- последовательные hard filters
- budget-slack scoring
- WHY NOT причины исключения
- what-if анализ
Особенности
Воронка отбора с числом профилей на каждом этапе и WHY NOT для каждого исключённого; what-if-пересчёт параметров, показывающий, какие кандидаты появятся при смягчении условий.
Стек
Python · FastAPI · Pydantic · Uvicorn · HTML · CSS · JavaScript · unittest
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-e823438b-stackattack.
README
README.mdEventDNA
Подбор event-подрядчиков Казахстана с объяснением каждого решения.
EventDNA помогает людям, планирующим частное или корпоративное мероприятие, выбрать подрядчика без просмотра длинного каталога. Пользователь задаёт условия, а сервис возвращает не более трёх подходящих профилей, показывает причины выбора и объясняет, почему остальные не попали в результат.
Проект команды stackattack для хакатона. Текущая версия — локально запускаемое веб-приложение с каталогом из 66 профилей, работающим поиском и детерминированным алгоритмом рекомендаций.
Что реализовано
| Возможность | Что делает текущая версия |
|---|---|
| Поиск через форму | Город, дата, формат мероприятия, категория и бюджет; дополнительно — длительность и язык |
| Значения из каталога | Варианты выбора загружаются из CSV через API |
| Обязательные фильтры | Исключают профили, не удовлетворяющие условиям; дата из busy_dates всегда исключает подрядчика |
| Детерминированный TOP-3 | Одинаковый запрос при неизменном каталоге даёт одинаковый порядок; если подходят два, один или никто, результат не дополняется |
| WHY THIS | Объяснения выбора на основании структурированных полей и расчётов |
| WHY NOT | Первый нарушенный фильтр для каждого исключённого профиля, группировка причин; отдельное объяснение для прошедших фильтры кандидатов ниже TOP-3 |
| Воронка отбора | Количество профилей после каждого этапа |
| Происхождение данных | Отображение признаков дополненных цены/города и синтетического профиля |
| WHAT IF через API | Пересчёт соседних сценариев тем же алгоритмом, сравнение появившихся и исчезнувших кандидатов |
| Состояния интерфейса | Загрузка, пустой результат, ошибки, повтор загрузки каталога и предупреждение об изменённых параметрах |
| Адаптивная вёрстка | Интерфейс для широких и мобильных экранов |
Как работает решение
-
Пользователь открывает сайт. Форма получает доступные города, категории, форматы и языки из
/api/filters. -
Пользователь задаёт условия и нажимает «Найти моих подрядчиков».
-
Frontend отправляет JSON в
/api/recommend; backend проверяет структуру и значения запроса. -
Алгоритм последовательно применяет фильтры:
Все профили → категория → город → формат → занятые даты → бюджет → длительность → язык → ranking → до 3 результатов -
Для каждого исключения сохраняется первая причина. Оставшиеся кандидаты сортируются по индексу запаса бюджета и ID.
-
Пользователь видит параметры запроса, воронку, карточки, объяснения WHY THIS и WHY NOT.
Текстовые значения сравниваются без учёта регистра, внешних и повторяющихся пробелов. Категория, формат и язык проверяются по отдельным значениям списков, а не по подстроке. Длительность и язык применяются только при заполнении соответствующих полей.
Формула ranking
Для профилей, прошедших все фильтры:
score = (budget − price_from_kzt) / budget
Чем больше запас относительно стартовой цены, тем выше место. При нулевом бюджете прошедшие фильтры профили получают score = 0. При одинаковом score порядок определяется по ID по возрастанию.
В интерфейсе индекс отображается на шкале от 0 до 100. Это индекс запаса бюджета, а не оценка качества услуг или вероятность успешного мероприятия. Отзывы, популярность, текст описания и флаги происхождения данных на ranking не влияют.
WHAT IF
POST /api/what-if пересчитывает независимо друг от друга:
- увеличение бюджета на 50 000, 100 000 и 200 000 ₸;
- перенос даты на 1, 2 и 3 дня вперёд, если дата остаётся допустимой;
- сокращение длительности на 1 и 2 часа, если она задана и результат больше нуля.
Ответ содержит исходный результат, результаты сценариев, added_ids, removed_ids и изменение общего числа подходящих кандидатов. Сравниваются все прошедшие фильтры профили, а не только TOP-3. Дополнительно возвращается до пяти предложений с положительным приростом количества кандидатов. В веб-интерфейсе WHAT IF пока не подключён; его можно проверить через /docs.
Технологии
| Компонент | Технологии |
|---|---|
| Backend | Python, FastAPI, Pydantic 2 |
| Сервер | Uvicorn |
| Данные и расчёты | CSV, стандартные модули Python csv, datetime, decimal, dataclasses |
| Frontend | HTML, CSS, обычный JavaScript, браузерный Fetch API |
| Тесты | unittest, FastAPI TestClient, HTTPX |
Зависимости backend перечислены в requirements.txt, зависимости тестов — в requirements-dev.txt.
AI/LLM-модели в текущем коде не используются. Выбор, ranking, объяснения и сценарии вычисляются обычным кодом. Внешние API, API-ключи, платные сервисы, база данных и сборка через Node.js для запуска не нужны. Для первоначальной установки Python-зависимостей потребуется доступ к интернету.
Архитектура проекта
Браузер: HTML / CSS / JavaScript
│ HTTP / JSON
▼
FastAPI: backend/main.py
│
┌────────┴──────────┐
▼ ▼
engine.py what_if.py
▲ │
└── повторный вызов ─┘
│
├── filters.py обязательные условия
├── scoring.py индекс и стабильная сортировка
└── explanations.py причины выбора и исключения
contractors.csv → data_loader.py → профили в памяти приложения
FastAPI также раздаёт frontend: интерфейс и API работают на одном адресе. CSV загружается при старте сервера; база данных не используется.
.
├── backend/
│ ├── __init__.py
│ ├── main.py # API, загрузка каталога и раздача frontend
│ ├── models.py # Contractor и проверка запроса
│ ├── data_loader.py # Чтение и нормализация CSV
│ ├── engine.py # Общий pipeline, воронка и TOP-3
│ ├── filters.py # Фильтры и занятые даты
│ ├── scoring.py # Ranking
│ ├── explanations.py # WHY THIS / WHY NOT
│ ├── what_if.py # Пересчёт сценариев
│ └── data/contractors.csv # Каталог из 66 профилей
├── frontend/
│ ├── index.html
│ ├── style.css
│ └── app.js # Форма, запросы и отображение результатов
├── tests/
│ ├── __init__.py
│ ├── helpers.py # Искусственные тестовые профили
│ ├── test_dataset.py
│ ├── test_filters.py
│ ├── test_recommendations.py
│ └── test_api.py
├── .gitignore
├── requirements.txt
├── requirements-dev.txt
├── run.py
└── README.md
Установка и запуск
Понадобятся Git, Python с модулем venv и браузер. Проект проверен на Python 3.9.6. Команды ниже приведены для macOS/Linux.
1. Скачайте репозиторий
git clone https://github.com/BAITC-Hacks/hack-e823438b-stackattack.git
cd hack-e823438b-stackattack
Если репозиторий уже скачан, откройте терминал в его корневой папке — там, где находится run.py.
2. Создайте и активируйте окружение
python3 -m venv .venv
source .venv/bin/activate
В Windows PowerShell вместо этих команд:
py -m venv .venv
.\.venv\Scripts\Activate.ps1
3. Установите зависимости
python -m pip install -r requirements.txt
4. Запустите приложение
python run.py
Оставьте терминал с сервером открытым:
- Сайт: http://127.0.0.1:8000
- Интерактивная документация API: http://127.0.0.1:8000/docs
- Состояние каталога: http://127.0.0.1:8000/health
При корректной загрузке текущего CSV /health возвращает:
{"status":"ok","contractors_loaded":66}
Открывайте сайт через адрес сервера, а не двойным щелчком по index.html. Для остановки сервера нажмите Ctrl+C в его терминале. После изменения CSV остановите сервер и снова выполните python run.py: горячая перезагрузка каталога не реализована.
Если порт 8000 занят предыдущим запуском, сначала остановите тот процесс. Если браузер показывает старую страницу, обновите её без кеша: ⌘ Shift R на macOS или Ctrl Shift R на Windows/Linux.
Как проверить решение: сценарий для жюри
Поиск через сайт
-
Откройте http://127.0.0.1:8000.
-
Заполните форму:
Поле Значение Город Алматы Дата 14 ноября 2026 года Формат свадьба Категория Фотограф Бюджет 400000 Длительность в дополнительных пожеланиях 8 часов Язык русский -
Нажмите «Найти моих подрядчиков».
Для CSV, включённого в репозиторий, ожидается следующий порядок:
| Место | ID | Имя | Цена от | Индекс запаса бюджета |
|---|---|---|---|---|
| 1 | HK-68220 | Рок Ли | 190 000 ₸ | 52,5 / 100 |
| 2 | HK-76268 | Сацуки Кусакабэ | 200 000 ₸ | 50 / 100 |
| 3 | HK-91112 | Мэгуми Фушигуро | 300 000 ₸ | 25 / 100 |
Воронка: 66 всего → 12 по категории → 8 по городу → 6 по формату → 3 по дате → 3 по бюджету → 3 по длительности → 3 по языку.
- Раскройте WHY THIS у карточки и WHY NOT под результатами: проверьте факты и причины исключения.
- Повторите тот же запрос — порядок должен сохраниться.
- Измените дату на 15 ноября 2026 года и повторите поиск. Останутся Рок Ли и Мэгуми Фушигуро. У Сацуки Кусакабэ эта дата присутствует в
busy_dates; в WHY NOT появится соответствующая причина. - Верните 14 ноября и установите бюджет 0. Выдача станет пустой, с объяснением и причинами отказа.
Результаты примера относятся к текущему каталогу и могут измениться после его редактирования.
Проверка API и WHAT IF
В /docs выберите POST /api/recommend, нажмите Try it out и отправьте:
{
"city": "Алматы",
"date": "2026-11-14",
"event_format": "свадьба",
"category": "Фотограф",
"budget": 400000,
"duration_hours": 8,
"language": "русский"
}
Отправьте тот же JSON в POST /api/what-if. В scenarios найдите дату 2026-11-15: для неё removed_ids содержит HK-76268, а eligible_delta равен -1.
Запрос из терминала macOS/Linux:
curl -X POST http://127.0.0.1:8000/api/recommend \
-H 'Content-Type: application/json' \
-d '{"city":"Алматы","date":"2026-11-14","event_format":"свадьба","category":"Фотограф","budget":400000,"duration_hours":8,"language":"русский"}'
Автоматические тесты
В активированном окружении:
python -m pip install -r requirements-dev.txt
python -m unittest discover -s tests -v
В репозитории 21 тест: загрузка и валидация CSV, фильтры, граничные значения, занятые даты, стабильность ranking, выдача от нуля до трёх профилей, объяснения, WHAT IF, валидация API, пустой каталог и доступность frontend через сервер. Тестовые профили отделены от основного каталога.
Данные и интеграции
Источник подбора — backend/data/contractors.csv, включённый в репозиторий. В нём 66 профилей и 17 категорий. Значения города: Алматы — 50 профилей, Астана — 15, «Зарубежье» — 1. Названия профилей хранятся в поле anon_name.
| Поля | Назначение |
|---|---|
id, anon_name | Идентификатор и имя профиля |
categories, city, event_formats, languages | Условия подбора |
price_from_kzt | Стартовая цена в тенге |
max_hours | Максимальная длительность работы |
busy_dates | Список занятых дат |
city_imputed, price_imputed | Признаки дополненных значений |
synthetic | Признак синтетического профиля |
description | Текст описания; не участвует в ranking |
Формат файла: UTF-8, разделитель колонок — запятая; списки внутри ячеек разделяются |. Даты записываются как YYYY-MM-DD, флаги — true/false без учёта регистра или 1/0.
Загрузчик проверяет обязательные колонки, уникальность ID, числа, даты и флаги. Пропуски не заменяются выдуманными значениями. Неизвестная цена исключает профиль; неизвестная длительность исключает его только при заданном ограничении длительности. В текущем каталоге max_hours отсутствует у 9 профилей.
Объект data_confidence содержит флаги происхождения, а не числовую оценку надёжности. Они не штрафуют профиль при сортировке.
HTTP API приложения
| Метод | Путь | Назначение |
|---|---|---|
| GET | /health | Статус и число загруженных профилей |
| GET | /api/filters | Доступные значения формы и диапазон цен |
| POST | /api/recommend | Запрос, воронка, рекомендации, причины исключения и их статистика |
| POST | /api/what-if | Исходный результат и пересчитанные сценарии |
Также сохранены алиасы /filters, /recommendations и /what-if. Ошибки валидации запроса возвращают HTTP 422. При пустом каталоге /health сообщает dataset_empty, а фильтры, поиск и WHAT IF возвращают HTTP 503. Некорректный CSV останавливает запуск с диагностикой.
Интеграций с внешними календарями, CRM, сервисами бронирования или AI-провайдерами в текущей версии нет.
Ограничения текущей версии
- Данные статичны. Нет синхронизации календарей и подтверждения доступности в реальном времени. Отсутствие даты в
busy_datesне гарантирует возможность бронирования. - Учитывается стартовая цена «от», а не итоговая смета, стоимость выезда или почасовой тариф. После выбора условия нужно уточнять у подрядчика.
- Ranking оценивает только запас бюджета. Он не измеряет опыт, качество работы, отзывы или репутацию.
- Подбор выполняется для одной категории за запрос. Совместная оптимизация команды подрядчиков и общего бюджета события отсутствует.
- WHY NOT сохраняет первую причину отказа по порядку фильтров, а не все возможные нарушения сразу.
- WHAT IF реализован в API, но отдельного пользовательского экрана для него нет.
- Свободный текст и AI/LLM-разбор запроса не реализованы.
- Нет регистрации, личного кабинета, бронирования, оплаты и административного интерфейса редактирования каталога.
- Данные загружаются в память при запуске; после изменения CSV сервер нужно перезапустить.
- Это локальный MVP. В репозитории нет конфигурации production-развёртывания или нагрузочных тестов.
Deployed-версия
Подтверждённой ссылки на публично развёрнутую версию в репозитории нет. Для демонстрации используйте локальный запуск по инструкции выше. Адрес 127.0.0.1:8000 доступен на компьютере, где запущен сервер, и не является публичной ссылкой.
Авторы коммитов
История коммитов
- 2026-09-23 16:27Merge remote-tracking branch 'origin/main'wazukeed
- 2026-09-23 16:27docs: add Russian project guide and hackathon demo instructionswazukeed
- 2026-09-23 16:25Update index.htmlsayazhan555
- 2026-09-23 16:16merge: reconcile identical contractor catalogue importswazukeed
- 2026-09-23 16:15feat: connect contractor search UI and import 66-profile cataloguewazukeed
- 2026-09-23 15:56UpdateAkmaralZhumabay
- 2026-09-23 15:37merge: reconcile team backend with validated recommendation enginewazukeed
- 2026-09-23 15:35fix: complete deterministic recommendation backend and API testswazukeed
Все коммиты (17)
- 2026-09-23 15:27Implement FastAPI for contractor recommendation APIAkmaralZhumabay
- 2026-09-23 15:26Implement what-if analysis for contractor eligibilityAkmaralZhumabay
- 2026-09-23 15:26Add rejection labels and explanation building logicAkmaralZhumabay
- 2026-09-23 15:24Implement contractor scoring and ranking functionsAkmaralZhumabay
- 2026-09-23 15:23Add contractor evaluation and normalization functionsAkmaralZhumabay
- 2026-09-23 15:23Refactor contractor data loading with pandasAkmaralZhumabay
- 2026-09-23 15:22Refactor contractor model to Pydantic and add requestsAkmaralZhumabay
- 2026-09-23 14:54feat: add responsive EventDNA landing interfacewazukeed
- 2026-09-23 14:18feat: scaffold EventDNA and add CSV loader with testswazukeed
Структура репозитория (24 файлов)
.gitignore README.md backend/__init__.py backend/data/contractors.csv backend/data_loader.py backend/engine.py backend/explanations.py backend/filters.py backend/main.py backend/models.py backend/scoring.py backend/what_if.py frontend/app.js frontend/index.html frontend/style.css requirements-dev.txt requirements.txt run.py tests/__init__.py tests/helpers.py tests/test_api.py tests/test_dataset.py tests/test_filters.py tests/test_recommendations.py
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.