AAYMA
RU
Войти

JJS

Streamlit-сервис, который выбирает до 3 подрядчиков из каталога на 66 профилей и объясняет выбор фактами.

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

О проекте

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

HackAlem 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

Сначала выбираются город и категория. Далее кандидаты последовательно проходят:

  1. Свободную дату.
  2. Поддержку формата мероприятия.
  3. Цену не выше бюджета.
  4. Поддержку языка, если он задан.
  5. Допустимую длительность, если она задана и ограничение применимо.

Текстовые значения сравниваются без учёта регистра и крайних пробелов. Бюджет запроса может быть нулевым, длительность — только положительной. Подрядчик, не прошедший фильтр, не может вернуться в выдачу за счёт высокого 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), а также с обсуждением команды.

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

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

Erkebulan 9kalybajnursejt49-coder 3

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

  • 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 комментариев

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

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