JJS
Streamlit-сервис, который выбирает до 3 подрядчиков из каталога на 66 профилей и объясняет выбор фактами.
О проекте
Команда: JJS
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Streamlit-сервис, который выбирает до 3 подрядчиков из каталога на 66 профилей и объясняет выбор фактами. Показывает воронку отсева, сравнение дат и проверенные подсказки: минимальный бюджет или ближайшую свободную дату.
Как устроено
recommender.py загружает и валидирует CSV, затем применяет жёсткие фильтры: город/категория → свободная дата → формат → бюджет → язык → длительность. Прошедших кандидатов ранжирует score = 0.60·TF-IDF-косинус (описание против запроса) + 0.25·budget_fit + 0.15·format_relevance. Объяснение собирается из фактов и самого релевантного фрагмента описания. При пустой выдаче сервисный слой перебирает бюджет или дату, меняя одно условие. LLM не используется.
Ключевые решения
- TF-IDF (stdlib)
- косинусная близость
- взвешенный скоринг
- жёсткие фильтры
- воронка диагностики
- перебор альтернатив по одному условию
Особенности
Полностью детерминированный и объяснимый скоринг без LLM: TF-IDF написан на стандартной библиотеке. Подсказки показываются только если проверены повторным вызовом ядра. Статусы category_absent и no_eligible разделены.
Стек
Python · Streamlit
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-c6ee1259-jjs.
README
README.mdHackAlem AI — умный подбор подрядчиков
Проект команды JJS для HackAlem AI, трек «Креативные индустрии». Сервис помогает организатору мероприятия выбрать до трёх подрядчиков из каталога по городу, дате, формату и бюджету. Для каждой рекомендации показаны конкретные причины выбора и подтверждающие факты профиля.
Это инструмент выбора: бронирования и отправки заявок подрядчикам нет.
Что реализовано
- Streamlit-интерфейс подключён к настоящему
src.recommender.recommend(request). - Загружается
project/data/contractors.csv: 66 профилей, 13 столбцов. - Mock-режим удалён; кнопки сценариев только заполняют форму.
- Жёсткие фильтры, детерминированное ранжирование и объяснения на основе данных.
- Три исхода: подобрали, категории нет, никто не подходит под все условия.
- Воронка отбора, сведения о происхождении данных и сравнение двух дат.
- Проверенные подсказки: минимальный бюджет или ближайшая последующая доступная дата.
- Проверки core и UI: 48 passed.
Запуск в Windows / VS Code
Проверенная среда: Python 3.14.7, Streamlit 1.64.0.
Версии пакетов в requirements.txt не закреплены.
Откройте корень репозитория в VS Code, затем Терминал → Создать терминал:
cd project
python -m pip install -r requirements.txt
python -m streamlit run app.py
Если терминал уже находится в project, повторять cd project не нужно.
Откройте Local URL, напечатанный Streamlit, обычно http://localhost:8501.
При вопросе Email: оставьте поле пустым и нажмите Enter.
Для остановки сервера нажмите Ctrl + C в терминале запуска.
Для изолированной установки можно использовать виртуальное окружение.
Из project, без активации и изменения политики PowerShell:
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe -m streamlit run app.py
После установки зависимостей подбор работает локально. API-ключи, внешние LLM-сервисы и отдельный HTTP-backend не требуются.
Интерфейс
Обязательные параметры: город, дата, тип мероприятия, категория подрядчика, бюджет в тенге. Длительность и язык можно не указывать. Календарь ограничен 23 сентября — 31 декабря 2026 года и в UI, и в core: за пределами этого периода доступность неизвестна.
Карточки показывают имя, категории, город, стартовую цену и объяснение. В блоке «Почему этот подрядчик» доступны форматы, языки, цена, длительность и исходное описание. В «Технических деталях» — ID, score и его компоненты.
Значки отражают происхождение данных:
- Синтетический профиль —
synthetic; - Цена восстановлена —
price_imputed; - Город восстановлен —
city_imputed.
Значки включаются для значений true, 1, yes без учёта регистра.
Цена «от» требует уточнения у подрядчика. Score не является оценкой качества
или вероятностью успешного мероприятия.
Архитектура
README.md
project/
├── app.py # точка входа Streamlit
├── ui/
│ ├── views.py # форма, карточки, воронка, сравнение
│ ├── presets.py # параметры четырёх demo-запросов
│ ├── presentation.py # понятные даты, цены, тексты объяснений и отсева
│ └── service.py # вызов core и проверка альтернативных условий
├── src/
│ ├── __init__.py # экспорт recommend
│ └── recommender.py # загрузка CSV, валидация, фильтры, ranking, explanations
├── tests/
│ ├── test_recommender.py # проверки loader и recommendation engine
│ └── test_polish.py # подсказки, календарь, тексты и Streamlit AppTest
├── data/
│ └── contractors.csv # каталог подрядчиков
├── requirements.txt
└── AGENTS.md # зоны ответственности команды
Форма → ui.service → src.recommender.recommend
→ CSV → жёсткие фильтры → ranking → до 3 рекомендаций
→ карточки, объяснения и воронка в UI
UI не пересортировывает результаты и не меняет score. При пустой выдаче сервисный
слой может выполнить дополнительные запросы к core для проверки подсказок.
Текущие запрос и ответ — словари; отдельного src/models.py нет.
TF-IDF реализован стандартной библиотекой Python. pandas и scikit-learn
перечислены в зависимостях, но текущий engine их не использует.
Данные
Core читает project/data/contractors.csv в UTF-8 с поддержкой BOM.
Каталог содержит следующие столбцы:
id,anon_name,categories,city,city_imputed,synthetic,price_from_kzt,price_imputed,event_formats,languages,max_hours,busy_dates,description
id— непустой уникальный идентификатор,anon_name— анонимизированное имя.categories— одна или несколько категорий.event_formats,languages,busy_dates— списки форматов, языков и занятых дат.- Списки поддерживают разделители
|,;или JSON-массив строк. price_from_kzt— конечное неотрицательное число.busy_datesиспользует форматYYYY-MM-DD.- Пустой
max_hours,null,NoneилиNaNозначает, что ограничение длительности не применяется; иначе требуется конечное неотрицательное число. synthetic,city_imputed,price_imputedсохраняются в ответе как строки CSV.
Loader использует реальные названия anon_name, categories, event_formats.
Переименовывать их в старые name, category, event_type не нужно.
Названия языков должны совпадать с запросом: автоматического перевода ru в
русский сейчас нет.
Отсутствующий файл или неверная схема вызывают ошибку загрузки, а не ложный статус «категории нет». UI сообщает об ошибке без подстановки временных данных.
Фильтрация и ranking
Сначала выбираются город и категория. Далее кандидаты последовательно проходят:
- Свободную дату.
- Поддержку формата мероприятия.
- Цену не выше бюджета.
- Поддержку языка, если он задан.
- Допустимую длительность, если она задана и ограничение применимо.
Текстовые значения сравниваются без учёта регистра и крайних пробелов. Бюджет запроса может быть нулевым, длительность — только положительной. Подрядчик, не прошедший фильтр, не может вернуться в выдачу за счёт высокого score.
Для подходящих кандидатов:
score = 0.60 × semantic_similarity
+ 0.25 × budget_fit
+ 0.15 × format_relevance
semantic_similarity— косинусная близость TF-IDF описания к запросу. Если передан непустойdescription, используется он; иначе —event_type. В текущей форме свободного описания нет, поэтому используется тип мероприятия.budget_fit = 1 − price_from_kzt / budget. При нулевом бюджете для бесплатного подходящего подрядчика компонент равен 1.format_relevance = 1 / число уникальных поддерживаемых форматов.
TF-IDF строится по описаниям прошедших кандидатов. Это лексическое сопоставление, без обучения нейросети и без внешних эмбеддингов.
Порядок: score по убыванию, затем цена по возрастанию, затем строковый id.
Возвращаются первые три кандидата. Тот же запрос на неизменном каталоге даёт тот же
порядок. Флаги synthetic/imputed сами по себе на ranking не влияют.
Объяснения, diagnostics и подсказки
Core формирует explanation из конкретных фактов: свободная дата, цена и бюджет, формат, язык и длительность. Дополняет его фрагментом описания с наибольшей TF-IDF-близостью к запросу. Если описания нет, это указывается явно.
ui/presentation.py делает технические формулировки понятнее: форматирует дату
и деньги, заменяет сообщение о max_hours обычным текстом. Факты и выбранная
цитата из описания сохраняются; LLM не используется.
| Статус core | Сообщение в интерфейсе |
|---|---|
matched | «Подобрано: …» и до трёх карточек |
category_absent | «В этом городе нет такой категории» |
no_eligible | «Никто не подходит под все условия» |
diagnostics содержит candidates, busy, format, budget, language,
duration, remaining. Профиль учитывается на первом проваленном фильтре.
remaining — число прошедших кандидатов до ограничения top-3.
UI строит воронку из этих счётчиков:
Кандидаты → свободны на дату → подходят по формату → проходят бюджет
→ проходят язык/длительность → рекомендованы
Последовательно вычитаются причины отсева, предпоследний этап равен remaining,
последний — длине recommendations. Причины показываются также обычным текстом.
Для no_eligible сервис проверяет альтернативы, меняя одно условие за раз:
- Если есть отсев по бюджету, перебирает более высокие цены каталога по возрастанию и находит минимальный бюджет, при котором core возвращает хотя бы один вариант.
- Если есть занятые кандидаты, проверяет следующие дни до конца календаря и предлагает ближайшую последующую дату с подходящим вариантом.
Другие условия сохраняются. Непроверенные подсказки не показываются; основной запрос и его результаты не подменяются альтернативами. Для вычисления подсказок может потребоваться несколько дополнительных вызовов core.
Контракт core
Пример вызова из папки project:
from src.recommender import recommend
response = recommend({
"city": "Алматы",
"date": "2026-09-30",
"event_type": "корпоратив",
"category": "Ведущий",
"budget": 1_200_000,
"duration": 6, # None, если не задана
"language": "русский", # None, если не задан
})
Ответ содержит status, recommendations, diagnostics. Рекомендация включает
поля профиля, score, score_components, explanation. UI-адаптер добавляет копию
request и словарь проверенных suggestions.
Demo-сценарии для жюри
Нажмите кнопку сценария, затем «Подобрать».
Ожидаемые результаты ниже проверены на каталоге ревизии ca00a70.
| Кнопка | Город / дата / формат / категория | Бюджет | Дополнительно | Результат |
|---|---|---|---|---|
| Плотная категория | Алматы / 2026-09-30 / корпоратив / Ведущий | 1 200 000 ₸ | 6 ч, русский | matched, 3 карточки из 6 подходящих |
| Редкая категория | Астана / 2026-09-23 / свадьба / Флорист | 350 000 ₸ | русский | matched, 1 карточка |
| Недостаточный бюджет | Алматы / 2026-09-23 / свадьба / Декоратор | 1 000 000 ₸ | русский | no_eligible; проверенная подсказка: бюджет от 2 000 000 ₸ |
| Категории нет | Астана / 2026-09-23 / корпоратив / Декоратор | 3 000 000 ₸ | язык и длительность не заданы | category_absent |
Для первого сценария воронка: 10 → 9 → 8 → 6 → 6 → 3.
Три карточки: HK-88430, HK-44923, HK-35215.
Во втором сценарии показан синтетический профиль HK-90002 с соответствующим значком.
В третьем один кандидат занят, два превышают бюджет.
Для сравнения включите «Сравнить 2026-09-30 и 2026-10-29» после выбора
первого сценария. Параметры совпадают, кроме даты. На 29 октября выдача:
HK-29829, HK-35215, HK-27222; воронка 10 → 8 → 7 → 5 → 5 → 3.
Изменение состава связано с календарями; ranking пересчитывается по прошедшим
кандидатам. Выбор кнопки сценария выключает режим сравнения.
Результаты не зашиты в UI и могут измениться при обновлении каталога.
Тесты
Из project:
python -m pytest -q
Для виртуального окружения:
.\.venv\Scripts\python.exe -m pytest -q
Подтверждённый результат: 48 passed. Проверяются схема реального CSV, списки и флаги, фильтры, top-3, детерминизм, ranking, объяснения, границы календаря, корректность альтернатив и UI. Часть тестов использует временные CSV, а Streamlit AppTest проверяет четыре demo-сценария и сравнение дат на каталоге проекта.
Для проверки только загрузки интерфейса:
python -c "from streamlit.testing.v1 import AppTest; app = AppTest.from_file('app.py').run(); assert not app.exception; print('UI startup OK')"
Частые проблемы
| Симптом | Что делать |
|---|---|
python не распознаётся | Полностью перезапустите VS Code после настройки PATH. Если доступен py, используйте py -V:3.14 -m streamlit run app.py |
pip или streamlit не распознаётся | Используйте python -m pip ... и python -m streamlit ... |
ModuleNotFoundError: No module named 'src' | Запускайте тесты и приложение из project, а не из корня репозитория |
| Каталог не найден | Проверьте project/data/contractors.csv: в имени не должно быть опечатки constractors.csv |
Loader требует старые name, category, event_type | У вас старая версия core; согласованно обновите код и каталог из репозитория |
Email: при первом запуске | Оставьте пустым и нажмите Enter |
| Порт 8501 занят | Остановите предыдущий сервер через Ctrl + C или добавьте --server.port 8502 |
Работа команды
Согласно project/AGENTS.md, участник core работает в src/, tests/, data/,
участник UI — в app.py, ui/, README.md. Чужие файлы не меняются без согласования.
Перед commit запускаются тесты; git push выполняется только по явной команде.
README сверён с кодом и каталогом локальной ревизии ca00a70 (polish),
а также с обсуждением команды.
Авторы коммитов
История коммитов
- 2026-09-23 17:56docs: update README for real recommendation enginekalybajnursejt49-coder
- 2026-09-23 17:49polishErkebulan
- 2026-09-23 17:35debugErkebulan
- 2026-09-23 17:29commit finalErkebulan
- 2026-09-23 17:24commit dataErkebulan
- 2026-09-23 17:15ui updatekalybajnursejt49-coder
- 2026-09-23 16:43uikalybajnursejt49-coder
- 2026-09-23 16:40backendErkebulan
Все коммиты (12)
- 2026-09-23 16:25EmptyErkebulan
- 2026-09-23 16:16AGENTS.mdErkebulan
- 2026-09-23 16:09project structureErkebulan
- 2026-09-23 15:52project structureErkebulan
Структура репозитория (14 файлов)
README.md project/AGENTS.md project/app.py project/data/contractors.csv project/requirements.txt project/src/__init__.py project/src/recommender.py project/tests/test_polish.py project/tests/test_recommender.py project/ui/__init__.py project/ui/presentation.py project/ui/presets.py project/ui/service.py project/ui/views.py
Ветки (2)
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.