AAYMA
RU
Войти

revent

Веб-интерфейс и FastAPI-сервис подбора до трёх подрядчиков для мероприятия: жёсткие фильтры и семантическое ранжирование по близости описания к…

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

О проекте

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

revent — подбор подрядчиков для мероприятий

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

Проект состоит из веб-интерфейса и API. Подбор выполняется по CSV-каталогу из репозитория; это демонстрация поиска, а не система бронирования.

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

  • Форма поиска: город, дата, формат мероприятия, категория и бюджет на одного подрядчика; язык и длительность необязательны.
  • Проверка условий: город и категория, занятость на дату, стартовая цена, поддерживаемый язык и максимальная длительность работы.
  • Семантическое ранжирование оставшихся профилей по близости описания к формату мероприятия.
  • До трёх карточек с именем, категорией, городом, ценой «от» и объяснением включения.
  • Объяснения на основе структурированных полей и коротких фрагментов описаний. Оценочная цена обозначается отдельно.
  • Отображение синтетических профилей, причин исключения, пустых результатов и ошибок запроса; повтор поиска после ошибки.
  • Валидация входных данных и ответа API, автоматические тесты backend и frontend.
  • Интерактивная документация API /docs и проверка готовности /health.

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

  1. Пользователь заполняет форму, например ищет ведущего в Алматы для корпоратива.
  2. Браузер проверяет поля и отправляет JSON в POST /api/find.
  3. Backend выбирает профили нужной категории и города. Затем исключает занятых, слишком дорогих и не соответствующих указанным языку или длительности.
  4. Формат мероприятия преобразуется в вектор. Для каждого оставшегося профиля вычисляется косинусная близость этого вектора к вектору полного описания.
  5. API сортирует результаты по убыванию близости; при равенстве — по строковому идентификатору. Возвращаются первые три профиля и причины исключения остальных неподходящих профилей.
  6. Интерфейс отображает карточки в порядке, полученном от API.

Формат мероприятия не является жёстким фильтром. Он используется для ранжирования: отсутствие точного совпадения в event_formats само по себе не исключает подрядчика.

Как формируется объяснение

Объяснение собирается обычным Python-кодом, без генеративной LLM:

  • бюджет, язык, длительность и точное совпадение формата подтверждаются структурированными полями;
  • для описания сначала ищутся точные слова запроса;
  • при отсутствии совпадения выбирается фрагмент описания с предпочтением сведений о стиле, опыте или специализации;
  • фрагменты с утверждениями о языке, цене или часах отбрасываются эвристическим фильтром;
  • длина цитаты ориентируется на 12 слов / 160 символов, но граница предложения или части предложения важнее лимита; многоточие не добавляется;
  • если пригодного фрагмента нет, предложение об описании опускается; при совпадении текстов используются альтернативный фрагмент или идентификатор профиля.

Цитата — это утверждение из исходного описания, а не независимо проверенный факт. Она также не является точным объяснением того, какие слова повлияли на оценку embedding-модели.

Технологии

КомпонентИспользуется в проекте
BackendPython, FastAPI, Uvicorn
Модели и валидацияPydantic 2
Семантический поискSentence Transformers, модель paraphrase-multilingual-MiniLM-L12-v2
Вычисление близостиNumPy; нормализация векторов и скалярное произведение
Зависимости MLPyTorch и Transformers присутствуют в backend/requirements.lock.txt
FrontendHTML, 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.

Для поставляемого каталога /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.

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

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

abzal 8Yernazar 7Adil 5

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

  • 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
Ветки (3)

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

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

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