Умный подбор подрядчиков
Streamlit-прототип подбора до трёх event-подрядчиков из каталога на 66 профилей: жёсткие фильтры, семантическое ранжирование и объяснения фактами из…
О проекте
Команда: Eclipse
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Streamlit-прототип подбора до трёх event-подрядчиков из каталога на 66 профилей: жёсткие фильтры, семантическое ранжирование и объяснения фактами из каталога.
Как устроено
Сначала ищутся город и категория, затем применяются жёсткие фильтры: занятые даты, бюджет, формат, язык, длительность; причины отказа агрегируются. Запрос и описания оставшихся профилей кодируются моделью intfloat/multilingual-e5-small (префиксы query:/passage:) и сравниваются по косинусной близости. Итоговый score складывается из семантики (0–70), запаса бюджета (0–20) и запаса по длительности (0–10), отбирается Top-3. Объяснение строится из самого релевантного предложения описания (с проверкой, что цитата есть в оригинале) и фактов о цене, языке и формате. LLM не используется.
Ключевые решения
- multilingual-e5-small embeddings
- cosine similarity
- hard constraint filtering
- deterministic weighted scoring
- sentence-level evidence extraction
Особенности
Сознательно обходятся без LLM и без обучения на малом каталоге: предобученные мультиязычные эмбеддинги сопоставляют RU/KZ-запросы с EN-описаниями без перевода. Ранжирование детерминированное, объяснения содержат только проверенные цитаты. Статусы category_not_found и no_eligible_candidates разделены, эмбеддинги кэшируются.
Стек
Python · Streamlit · sentence-transformers · pytest
Модели
intfloat/multilingual-e5-small
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-20220531-eclipse.
README
README.mdУмный подбор подрядчиков
Небольшой Streamlit-прототип подбора event-подрядчиков по анонимизированному каталогу из 66 профилей. Приложение проверяет доступность и обязательные условия, ранжирует только подходящие профили и объясняет рекомендации фактами из каталога.
Как работает
User request
→ City / category lookup
→ Hard constraints (availability, budget, event format, language, duration)
→ Eligible candidates
→ Pretrained multilingual embeddings
→ Semantic similarity
→ Deterministic score
→ Top 3
→ Evidence-based explanations
Сначала выполняется поиск города и категории. Если совпадений нет, возвращается category_not_found. Затем для каждого профиля проверяются все фильтры, причины отказа сохраняются и агрегируются. Если совпадения категории есть, но все профили отсеяны, возвращается отдельный статус no_eligible_candidates.
Счётчики причин отказа могут пересекаться: например, один профиль одновременно занят и выше бюджета.
Фильтры
- Дата: профиль исключается, если выбранная дата есть в busy_dates.
- Бюджет: исключается профиль, у которого price_from_kzt выше бюджета. Это стартовая цена, а не финальная стоимость.
- Формат: выбранный тип должен присутствовать в event_formats.
- Язык: выбранный язык должен присутствовать в languages; вариант «Не важно» отключает фильтр.
- Длительность: профиль исключается, когда числовой max_hours меньше запроса. Пустой max_hours трактуется как независимая от времени на площадке услуга.
Ранжирование и объяснения
Семантическая релевантность считается pretrained мультиязычной моделью intfloat/multilingual-e5-small. Fine-tuning на каталоге не выполняется. Для запроса используется префикс query:, для профиля — passage:. Embeddings нормализуются при encode, после чего cosine similarity считается dot product. Запрос содержит категорию, формат и при наличии короткий контекст; город, бюджет, длительность и язык остаются hard filters. Описание подрядчика — основной источник semantic similarity. Ручной перевод не используется.
Итоговый score складывается из:
- релевантность описания: 0–70 баллов;
- запас между бюджетом и стартовой ценой: 0–20 баллов;
- запас по длительности или независимость от времени на площадке: 0–10 баллов, если длительность указана.
Формулы для уже прошедших фильтры кандидатов:
semantic_points = 70 * clip(query_embedding @ profile_embedding, 0, 1)
budget_points = 20 * clip((budget - price) / budget, 0, 1)
duration_points = clip(5 + max_hours - requested_hours, 0, 10)
score = semantic_points + budget_points + duration_points
Если бюджет равен нулю или цена отсутствует, бонус бюджета равен нулю. При неуказанной длительности бонус равен нулю; при указанной длительности и max_hours=null — 10. Семантика имеет наибольший диапазон баллов, но итог учитывает все три фактора: более высокий raw similarity сам по себе не гарантирует первое место при разных ценах и лимитах часов. Баллы не являются вероятностью качества подрядчика.
Модель работает на CPU в eval / inference mode, без обучения. Цена не может компенсировать провал обязательного фильтра. Кандидаты сортируются по score по убыванию, а при равенстве — по строковому id по возрастанию. Случайная генерация и LLM для выбора Top 3 не используются.
Тексты и кэширование
embedding_service.py строит короткий запрос: query: Категория подрядчика: … Формат мероприятия: … Контекст: …. Контекст необязателен и ограничен 300 символами. Город, сумма бюджета, дата, часы и язык в semantic text не входят.
Профиль: passage: Описание: … Категория: … Форматы мероприятий: …. Описание идёт первым, чтобы длинные метаданные не вытеснили его при ограничении длины модели. ID, цены, даты занятости, признаки synthetic/imputed и язык в embedding не попадают. Отсутствующее описание допустимо: используются категория и формат, а цитата в объяснении отсутствует.
В app_resources.py:
get_embedding_modelиспользуетst.cache_resource: одна модель на процесс Streamlit.get_contractor_embeddingsиспользуетst.cache_data: матрица 66 × 384 в порядке каталога. Ключ включает тексты и имя модели.get_sentence_embeddingsиспользуетst.cache_data: словарь векторов уникальных предложений описаний с префиксомpassage:.get_contractorsкэширует нормализованные записи; изменение времени модификации файла обновляет каталог.
Embeddings вычисляются пакетами по 32 текста. При запросе код фильтрует кандидатов, берёт только соответствующие строки матрицы и один раз кодирует query. Тот же query vector используется для выбора предложения у каждого кандидата Top 3. Если все кандидаты отсеяны, query вообще не кодируется.
Кэши Streamlit хранятся в памяти процесса: повторные взаимодействия их используют, перезапуск сервера заново вычисляет векторы. Веса модели хранятся на диске в стандартном кэше Hugging Face. Загрузчик сначала пробует local_files_only=True: при полном локальном кэше обращения к сети не нужны. При первом запуске SentenceTransformer скачает pretrained multilingual embedding model; после этого модель работает локально, внешний LLM API для рекомендаций не требуется. Никакого training или fine-tuning не происходит.
Каждое объяснение собирается локально из реальных фактов профиля. Самое релевантное предложение выбирается по cosine similarity к запросу; при равенстве выбирается первое. Цитата проверяется на наличие в исходном описании. Добавляется факт о языке, длительности, цене или формате; шаблоны не приписывают профилю рейтинг, награды или неподтверждённые навыки. Карточки показывают Synthetic profile либо Dataset profile; восстановленные цена и город помечаются отдельно.
В боковой панели включите «Показать детали ranking»: появятся исходный запрос, статистика фильтрации, время ответа, similarity, баллы по каждому фактору, итоговый score и выбранное предложение. Переключение сохраняет результаты последнего запроса. Смена города сразу обновляет категории.
Файлы каталога
В исходных материалах каталог предоставлен как CSV, поэтому приложение загружает data/hackathon-dataset-anonymized.csv. Загрузчик также принимает JSONL/NDJSON, если такой файл будет передан явно.
Запуск
Требуется Python 3.11 или новее.
На Windows можно дважды щёлкнуть start.cmd в папке проекта. Он использует work\.venv, при необходимости создаёт окружение через Python Launcher, устанавливает зависимости и запускает Streamlit. Окно терминала нужно оставить открытым.
Или из PowerShell в папке проекта:
# Только если окружения ещё нет:
py -3 -m venv .\work\.venv
# Установка и запуск:
& '.\work\.venv\Scripts\python.exe' -m pip install -r requirements.txt
& '.\work\.venv\Scripts\python.exe' -m streamlit run app.py
Путь содержит отдельные каталоги work и .venv, разделённые \. Активировать окружение не требуется. Откройте адрес из терминала, обычно http://localhost:8501. Остановка — Ctrl+C. При первом скачивании модели нужен интернет и время на загрузку весов; после загрузки warm query должен занимать менее 10 секунд.
Для другого ОС/окружения: python -m pip install -r requirements.txt, затем python -m streamlit run app.py.
В .streamlit/config.toml отключён наблюдатель файлов: его обход lazy-модулей transformers мог загружать необязательные vision/audio зависимости и создавать ошибки об отсутствии torchvision. Для текстовой E5 они не нужны. После редактирования Python-кода перезапустите сервер вручную; взаимодействия с формой и кэширование работают как обычно.
Проверки
# Быстрые проверки без настоящей модели:
& '.\work\.venv\Scripts\python.exe' -m pytest -p no:cacheprovider -q
# Полный набор, включая настоящую E5 и интерфейс Streamlit:
& '.\work\.venv\Scripts\python.exe' -m pytest -p no:cacheprovider -q --run-model-tests
# Измерения и реальные demo-запросы:
& '.\work\.venv\Scripts\python.exe' validate_demo.py
Unit tests используют тестовую модель для бизнес-правил; они сами по себе не доказывают качество pretrained embeddings. Integration tests отдельно загружают настоящую E5: проверяют semantic relevance, сопоставление RU/KZ-запросов с EN-описаниями, релевантное предложение, детерминизм, реальные даты, пустые описания и время ответа. Тест интерфейса нажимает кнопки Streamlit, меняет город/категорию, проверяет результаты и отсутствие повторного encode при переключении debug.
validate_demo.py сохраняет фактически измеренные результаты в outputs/validation_report.json: три повторения каждого запроса, Top 3 с баллами и evidence, времена, форму матрицы и пример изменения порядка за счёт семантики. Передача --run-model-tests требует установленного sentence-transformers и доступных весов (кэш либо интернет для первой загрузки).
Why embeddings instead of model training?
We do not train a model on the 66 contractor profiles because the dataset is too small for meaningful model training. Instead, we use a pretrained multilingual embedding model. The dataset is only a contractor catalog; it is not a training set.
First, deterministic business rules remove contractors who are unavailable, over budget, or do not support the event format, language or requested duration. Then embeddings measure semantic similarity between the customer's request and the descriptions of the remaining contractors. This semantic score is combined with additional deterministic ranking factors. The final Top 3 is therefore both explainable and deterministic.
Each contractor description is converted into an embedding vector. The customer request is converted into another embedding vector. Cosine similarity measures semantic relevance across Russian, Kazakh and English without manual translation. There is no external LLM API and no stochastic candidate selection.
Справочники: модель multilingual-e5-small, кэширование данных Streamlit.
Demo scenarios на исходном каталоге
Сценарии используют фактические профили каталога. Длительность можно оставить неуказанной.
A — плотная категория
- Город: Алматы
- Категория: Ведущий
- Дата: 2026-11-14
- Формат: корпоратив
- Бюджет: 1 500 000 ₸
- Длительность: 5 часов
- Язык: русский
В категории найдено 10 профилей; 4 проходят все фильтры. Пять профилей заняты на эту дату, у одного стартовая цена выше бюджета, ещё один не работает с корпоративами. Приложение ранжирует прошедшие фильтры профили по описаниям и показывает Top 3.
B — редкая категория
- Город: Астана
- Категория: Флорист
- Дата: 2026-11-14
- Формат: корпоратив
- Бюджет: 350 000 ₸
- Язык: русский
В каталоге один астанинский флорист — Хаку (HK-90002); у его профиля указана стартовая цена от 300 000 ₸.
C — категория есть, результатов нет
Повторите сценарий B с бюджетом 100 000 ₸. Категория существует, но стартовая цена единственного профиля выше бюджета, поэтому приложение покажет no_eligible_candidates и причину отказа.
Проверка смены даты
Выполните сценарий B на 2026-11-02, затем на 2026-11-14, оставив остальные параметры прежними. Единственный астанинский флорист HK-90002 занят 2 ноября и свободен 14 ноября, поэтому сначала рекомендаций не будет, а затем появится одна.
Авторы коммитов
История коммитов
- 2026-09-23 17:49Add files via uploadSherkhan017
Структура репозитория (12 файлов)
.gitignore README.md app.py app_resources.py data_loader.py embedding_service.py explanation.py pytest.ini recommender.py requirements.txt start.cmd validate_demo.py
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.