revent
Веб-интерфейс и FastAPI-сервис подбора до трёх подрядчиков для мероприятия: жёсткие фильтры и семантическое ранжирование по близости описания к…
О проекте
Команда: kaspi
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Веб-интерфейс и FastAPI-сервис подбора до трёх подрядчиков для мероприятия: жёсткие фильтры и семантическое ранжирование по близости описания к формату события с объяснениями.
Как устроено
Backend выбирает профили нужной категории и города, исключает занятых, дорогих и не подходящих по языку и длительности. Формат мероприятия кодируется sentence-transformers (paraphrase-multilingual-MiniLM-L12-v2), кандидаты сортируются по косинусной близости к вектору полного описания; формат не жёсткий фильтр. Объяснения собираются Python-кодом из структурированных полей и коротких цитат описания с эвристическим отсевом утверждений о цене/языке/часах, без генеративной LLM.
Ключевые решения
- жёсткие фильтры
- семантическое ранжирование эмбеддингами
- косинусная близость
- экстрактивные объяснения
- кэш векторов запросов
Особенности
Эмбеддинги используются только для ранжирования, а не как фильтр; README честно документирует известный падающий фронтенд-тест из-за регистра «Корпоратив».
Стек
Python · FastAPI · Uvicorn · Pydantic 2 · Sentence Transformers · NumPy · HTML/CSS/JS · Playwright · Docker
Модели
paraphrase-multilingual-MiniLM-L12-v2
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-0e1b883f-kaspi.
README
README.mdrevent — подбор подрядчиков для мероприятий
Прототип сервиса для организаторов мероприятий: пользователь задаёт условия события и получает до трёх подрядчиков с объяснением, почему они попали в подборку. Решение помогает сократить ручной просмотр каталога и сравнить исполнителей по бюджету, доступности, языку и длительности работы.
Проект состоит из веб-интерфейса и API. Подбор выполняется по CSV-каталогу из репозитория; это демонстрация поиска, а не система бронирования.
Что реализовано
- Форма поиска: город, дата, формат мероприятия, категория и бюджет на одного подрядчика; язык и длительность необязательны.
- Проверка условий: город и категория, занятость на дату, стартовая цена, поддерживаемый язык и максимальная длительность работы.
- Семантическое ранжирование оставшихся профилей по близости описания к формату мероприятия.
- До трёх карточек с именем, категорией, городом, ценой «от» и объяснением включения.
- Объяснения на основе структурированных полей и коротких фрагментов описаний. Оценочная цена обозначается отдельно.
- Отображение синтетических профилей, причин исключения, пустых результатов и ошибок запроса; повтор поиска после ошибки.
- Валидация входных данных и ответа API, автоматические тесты backend и frontend.
- Интерактивная документация API
/docsи проверка готовности/health.
Как работает решение
- Пользователь заполняет форму, например ищет ведущего в Алматы для корпоратива.
- Браузер проверяет поля и отправляет JSON в
POST /api/find. - Backend выбирает профили нужной категории и города. Затем исключает занятых, слишком дорогих и не соответствующих указанным языку или длительности.
- Формат мероприятия преобразуется в вектор. Для каждого оставшегося профиля вычисляется косинусная близость этого вектора к вектору полного описания.
- API сортирует результаты по убыванию близости; при равенстве — по строковому идентификатору. Возвращаются первые три профиля и причины исключения остальных неподходящих профилей.
- Интерфейс отображает карточки в порядке, полученном от API.
Формат мероприятия не является жёстким фильтром. Он используется для ранжирования: отсутствие точного совпадения в event_formats само по себе не исключает подрядчика.
Как формируется объяснение
Объяснение собирается обычным Python-кодом, без генеративной LLM:
- бюджет, язык, длительность и точное совпадение формата подтверждаются структурированными полями;
- для описания сначала ищутся точные слова запроса;
- при отсутствии совпадения выбирается фрагмент описания с предпочтением сведений о стиле, опыте или специализации;
- фрагменты с утверждениями о языке, цене или часах отбрасываются эвристическим фильтром;
- длина цитаты ориентируется на 12 слов / 160 символов, но граница предложения или части предложения важнее лимита; многоточие не добавляется;
- если пригодного фрагмента нет, предложение об описании опускается; при совпадении текстов используются альтернативный фрагмент или идентификатор профиля.
Цитата — это утверждение из исходного описания, а не независимо проверенный факт. Она также не является точным объяснением того, какие слова повлияли на оценку embedding-модели.
Технологии
| Компонент | Используется в проекте |
|---|---|
| Backend | Python, FastAPI, Uvicorn |
| Модели и валидация | Pydantic 2 |
| Семантический поиск | Sentence Transformers, модель paraphrase-multilingual-MiniLM-L12-v2 |
| Вычисление близости | NumPy; нормализация векторов и скалярное произведение |
| Зависимости ML | PyTorch и Transformers присутствуют в backend/requirements.lock.txt |
| Frontend | HTML, CSS, JavaScript с ES-модулями и Fetch API, без сборщика |
| Данные | Локальный CSV; сервер базы данных не используется |
| Тестирование | Python unittest, FastAPI TestClient / HTTPX, Node.js node:test, Playwright / Chromium |
Модель загружается через Sentence Transformers при запуске; при отсутствии локального кеша требуется скачивание весов. Вызовов генеративных AI API, платёжных сервисов и внешних календарей в коде нет. API-ключи для текущего сценария не предусмотрены.
Архитектура
Браузер: frontend/index.html + src/app.js
│ проверка формы: src/domain.js
│ POST /api/find: src/api.js
▼
FastAPI: backend/app/main.py
├── models.py — проверка запроса и схема ответа
├── filters.py — отбор по обязательным условиям
└── ranking.py — векторы, ранжирование и объяснения
▲
│ профили загружаются при старте
data.py ← backend/data/contractors.csv
При старте процесса backend валидирует CSV и один раз вычисляет векторы описаний. Векторы запросов кешируются в памяти, до 256 значений на экземпляр ранжировщика. После изменения CSV сервер нужно перезапустить. Ошибка загрузки данных или модели препятствует успешному запуску.
Основные файлы:
backend/
app/ # API, фильтры, модель данных, ранжирование
data/contractors.csv # каталог, используемый по умолчанию
tests/ # тесты данных, API и объяснений
requirements.txt # зависимости приложения
requirements-dev.txt # зависимости приложения и HTTPX для тестов
requirements.lock.txt # зафиксированный набор версий
frontend/
index.html # форма и область результатов
styles.css # оформление
src/ # логика формы, API и отображения
tests/ # проверки логики и браузерный сценарий
API_CONTRACT.md # контракт взаимодействия с backend
Установка и запуск
Нужны Python 3.10+ с pip и venv, современный браузер и доступ в интернет для установки зависимостей и первой загрузки модели. Команды ниже рассчитаны на Bash в Linux/macOS и выполняются из корня локальной копии репозитория. Node.js нужен только для фронтенд-тестов.
1. Запустить backend
cd backend
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m uvicorn app.main:app --host 127.0.0.1 --port 8001
При первом запуске дождитесь окончания загрузки модели и сообщения Application startup complete.
- Проверка готовности: http://127.0.0.1:8001/health.
- Swagger UI: http://127.0.0.1:8001/docs.
Для поставляемого каталога /health возвращает:
{"status":"ok","contractors":66,"synthetic_count":13}
2. Запустить frontend
В другом терминале, из корня репозитория:
cd frontend
python3 -m http.server 8000 --bind 127.0.0.1
Откройте http://127.0.0.1:8000/. Отдельная установка npm-зависимостей для работы интерфейса не нужна.
Адрес API задан в frontend/src/api.js константой API_BASE_URL: http://127.0.0.1:8001. Если backend работает по другому адресу, измените эту константу. Backend разрешает CORS-запросы со всех источников.
3. Подключить другой CSV при необходимости
Из каталога backend, с активированным окружением:
CONTRACTORS_PATH=/absolute/path/contractors.csv python -m uvicorn app.main:app --host 127.0.0.1 --port 8001
Файл должен иметь те же столбцы и формат значений, что и поставляемый каталог.
Как проверить решение
Сценарий для жюри
После запуска обоих компонентов задайте в форме:
| Поле | Значение |
|---|---|
| Город | Алматы |
| Дата | 14 ноября 2026 года |
| Формат | Корпоратив |
| Категория | Ведущий |
| Бюджет | 1 000 000 ₸ |
| Длительность | 4 часа |
| Язык | русский |
Нажмите «Подобрать подрядчиков». На текущем CSV условия проходят пять профилей; API показывает три из них после ранжирования. Ещё пять профилей выбранной категории и города исключены. Причины: Занят на выбранную дату: 5 и Цена выше бюджета: 2. Причины могут пересекаться, поэтому их сумма больше числа исключённых профилей.
Проверьте наличие объяснений и стартовых цен. Затем уменьшите бюджет до 1 ₸: ожидается пустая подборка со статусом no_match и причинами исключения.
Тот же основной запрос напрямую к API:
curl -X POST http://127.0.0.1:8001/api/find \
-H 'Content-Type: application/json' \
-d '{
"city": "Алматы",
"event_date": "2026-11-14",
"event_format": "корпоратив",
"category": "Ведущий",
"budget_kzt": 1000000,
"duration_hours": 4,
"language": "русский"
}'
Ответ содержит status, candidates, excluded_count, excluded_reasons. В каждой карточке: id, name, category, city, price_from_kzt, explanation, synthetic, price_imputed, city_imputed. Числовая оценка семантической близости в публичный ответ не включается.
Возможные статусы: ok — найдены подходящие профили; no_category_in_city — в городе нет указанной категории; no_match — профили есть, но не проходят условия. Некорректные параметры запроса приводят к HTTP 422.
Автоматические проверки
Backend, из корня репозитория:
cd backend
source .venv/bin/activate
python -m pip install -r requirements-dev.txt
python -m unittest discover -s tests -v
Тесты используют подставной encoder: они проверяют фильтры, API, кеширование, порядок результатов и объяснения, но не измеряют качество настоящей embedding-модели. Для проверки загрузки модели и полного рабочего сценария используйте запуск API и запрос выше.
Frontend, в отдельном терминале из корня репозитория:
cd frontend
node --test tests/domain.test.mjs
Браузерный сценарий требует работающего frontend-сервера, Playwright и Chromium. Из каталога frontend:
npm install --no-save --package-lock=false playwright
npx playwright install chromium
node tests/browser.test.mjs
Этот сценарий подменяет ответы API и проверяет поведение интерфейса, а не реальное ранжирование.
Известная проблема тестов frontend: тестовые входные данные используют корпоратив со строчной буквы, а текущий список формы содержит Корпоратив. Из-за этого проверка входных данных в domain.test.mjs падает; браузерный сценарий также выбирает отсутствующее значение корпоратив. Ручной сценарий выше использует фактическое значение из формы. Backend принимает оба варианта.
Данные и интеграции
Источник — анонимизированный CSV хакатона. Рабочий файл — backend/data/contractors.csv; в репозитории также присутствует backend/hackathon dataset anonymized .csv.
В рабочем каталоге:
- 66 профилей, из них 13 с признаком
synthetic; - 18 профилей с признаком восстановленной/оценочной цены
price_imputed; - 8 профилей с признаком восстановленного города
city_imputed; - у 9 профилей не указана максимальная длительность
max_hours.
Каталог содержит идентификатор, анонимизированное имя, категории, город, стартовую цену, форматы, языки, максимальную длительность, занятые даты и текстовое описание. Многозначные поля categories, event_formats, languages, busy_dates разделены символом |; загрузчик преобразует их в списки.
busy_dates — занятые, а не свободные даты. Отсутствие даты в списке трактуется алгоритмом как отсутствие занятости в этом наборе данных. Актуальное расписание через внешние сервисы не проверяется.
Интерфейс обращается только к API этого проекта. Интеграции с CRM, платёжными системами, сервисами бронирования или внешними каталогами в текущем коде не реализованы.
Ограничения текущей версии
- Каталог статический и небольшой; профили анонимизированы, часть данных синтетическая или восстановленная. Это не проверенная база действующих поставщиков.
- Цена указана «от» за мероприятие; она не является окончательной сметой и не пересчитывается пропорционально часам.
- Нет бронирования, оплаты, связи с исполнителем, авторизации, личного кабинета и интерфейса редактирования каталога.
- Форма ограничена датами с 23.09.2026 по 31.12.2026, тремя вариантами города и фиксированными списками категорий, форматов и языков. API не накладывает такое же календарное ограничение; даты за пределами данных не подтверждают реальную доступность.
- Если длительность указана, профиль с неизвестным
max_hoursисключается. Язык API принимает русским названием или алиасамиRU,KZ,KK,EN. - Семантическая близость не гарантирует соответствие формату. Порог минимальной близости не задан; метрик качества и оценки на размеченной выборке в репозитории нет.
- Объяснения строятся эвристически. Проверка противоречий в свободном тексте неполная, цитаты не верифицируются. При отсутствии ближайшей границы предложения цитата может превысить целевую длину.
- Возвращается максимум три профиля; пагинации и сохранения истории поиска нет.
- Веса модели и векторы находятся в памяти процесса; для первого запуска может понадобиться загрузка модели. Время ответа и нагрузочные характеристики в репозитории не подтверждены.
- Настройка CORS открыта для всех источников; готовая конфигурация промышленного развёртывания в репозитории не представлена.
Развёрнутая версия
Подтверждённая ссылка на публично развёрнутую версию в репозитории отсутствует. Для демонстрации используйте локальный запуск: интерфейс и документация API.
Авторы коммитов
История коммитов
- 2026-09-23 17:47remove .ideaYernazar
- 2026-09-23 17:43Change designAdil
- 2026-09-23 17:41change READMEYernazar
- 2026-09-23 17:40Change designAdil
- 2026-09-23 17:37polish comment logicYernazar
- 2026-09-23 17:24feat: add Dockerfile for backendabzal
- 2026-09-23 17:23Change designAdil
- 2026-09-23 17:11Lower case characterAdil
Все коммиты (20)
- 2026-09-23 17:01commit html fffabzal
- 2026-09-23 16:59Merge remote-tracking branch 'origin/main'abzal
- 2026-09-23 16:59commit htmlabzal
- 2026-09-23 16:58backend fixYernazar
- 2026-09-23 16:57commit htmlabzal
- 2026-09-23 16:02connect backend and frontendYernazar
- 2026-09-23 15:32added frontendAdil
- 2026-09-23 15:01Merge pull request #1 from BAITC-Hacks/codex/contractor-rankingabzalsag
- 2026-09-23 14:59Integrate CSV dataset, hard filters and semantic ranking into find APIabzalsag
- 2026-09-23 14:46Add semantic contractor ranking with factual explanations and testsabzalsag
- 2026-09-23 14:22initYernazar
- 2026-09-23 10:52testYernazar
Структура репозитория (28 файлов)
README.md backend/.gitignore backend/README.md backend/app/data.py backend/app/filters.py backend/app/main.py backend/app/models.py backend/app/ranking.py backend/data/contractors.csv backend/hackathon dataset anonymized .csv backend/requirements-dev.txt backend/requirements.lock.txt backend/requirements.txt backend/requirments.txt backend/tests/test_api.py backend/tests/test_data.py backend/tests/test_language_explanations.py backend/tests/test_ranking.py frontend/.gitignore frontend/API_CONTRACT.md frontend/README.md frontend/index.html frontend/src/api.js frontend/src/app.js frontend/src/domain.js frontend/styles.css frontend/tests/browser.test.mjs frontend/tests/domain.test.mjs
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.