AAYMA
RU
Войти

Умный подбор подрядчиков

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

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

О проекте

Команда: Nuravi
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP

Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор

Суть

Streamlit-приложение подбирает до 3 event-подрядчиков из каталога (66 профилей) по свободному тексту или ручной форме и объясняет выбор, сравнивая топ-1 с топ-2 по компонентам оценки.

Как устроено

LLM разбирает свободный текст заявки в поля и задаёт уточняющие вопросы (город, дата, формат, бюджет). Затем локальное ядро применяет 7 жёстких фильтров (категория, город, календарь, бюджет, формат, языки, часы) с воронкой причин отказа и три исхода (found/no_category/no_matches), и ранжирует по прозрачной формуле score = 100·Σ(weight·factor)/Σ(active_weights). Пожелание сопоставляется с фразами описания и опционально через embeddings, с проверкой дословных цитат.

Ключевые решения

  • hard-filter funnel
  • weighted scoring
  • embeddings similarity
  • quote verification
  • Decimal arithmetic
  • LLM intake/slot filling

Особенности

Предварительные профили показываются уже по категории, но без даты не объявляются свободными; пустой busy_dates трактуется как «доступность неизвестна». Отдельный подсчёт причин отказа по всем условиям одновременно и сравнение «что меняется на другой дате».

Стек

Python · Streamlit

Модели

OpenAI · NVIDIA API


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-ab3cdd19-nuravi.

README

README.md

Умный подбор подрядчиков · HackAlem

Рабочее демо команды Nuravi: до трёх подрядчиков из предоставленного каталога Казахстана с индивидуальными, проверяемыми объяснениями. На первом экране пользователь описывает задачу своими словами. Уже по категории приложение показывает предварительные профили, а ИИ последовательно уточняет город, дату, формат и бюджет; язык, часы и пожелание необязательны. Предварительные профили не являются рейтингом, а без даты не объявляются свободными. После уточнений запускается обычный подбор: блок «Почему рекомендован» показывает основания выбора; топ-1 сравнивается с топ-2 по реальным компонентам оценки. Каталог не дополнялся новыми профилями.

Запуск

Windows PowerShell из папки репозитория:

powershell -NoProfile -ExecutionPolicy Bypass -File .\run.ps1

Linux/macOS или Git Bash:

sh run.sh

Откройте http://127.0.0.1:8501. Скрипты создают локальную .venv, устанавливают зависимости и запускают Streamlit на localhost. Нужен Python 3.10+; проверено на Python 3.12.10. Первая установка требует интернета; подбор работает локально. Команда PowerShell выше разрешает выполнение только для запущенного процесса и не меняет политику системы. Другой порт: powershell -NoProfile -ExecutionPolicy Bypass -File .\run.ps1 --server.port 8502 или sh run.sh --server.port 8502. Остановка: Ctrl+C.

Ручная установка без uv:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m streamlit run app.py --server.address 127.0.0.1

Подбор через ручную форму работает без ключей. Для поиска свободным текстом скопируйте .env.example в .env в корне репозитория и заполните OPENAI_API_KEY или NVIDIA_API_KEY. При наличии обоих ключей OpenAI используется первым, NVIDIA — резервным провайдером. Уже существующий локальный tests/.env тоже читается. Оба .env игнорируются Git. Секреты не выводятся на страницу и не входят в аудит. Текст заявки и уже извлечённые поля отправляются провайдеру только после нажатия «Найти подрядчика». Отдельный переключатель в ручной форме включает AI-сравнение пожелания с описаниями подходящих профилей. Телеметрия Streamlit отключена.

Публичная демонстрация

Для Streamlit Community Cloud укажите GitHub-репозиторий, ветку main и entrypoint app.py. requirements.txt, .streamlit/config.toml, ui.css и data/contractors.csv уже находятся в репозитории. В настройках Secrets задайте корневой ключ OPENAI_API_KEY = "..."; не добавляйте реальный ключ в Git или README. Корневые секреты Streamlit доступны приложению как переменные окружения. После публикации проверьте внешнюю ссылку без входа в аккаунт: плотную категорию, смену даты, редкую категорию, пустой результат и AI-пожелание. Если репозиторий приватный, у развёртывающего аккаунта должны быть права администратора и приложению нужно отдельно включить публичный доступ; иначе жюри увидит запрос авторизации. URL из 127.0.0.1 пригоден только на вашем компьютере.

Демо для жюри

На первом экране — компактное поле «Кого вы ищете?». Можно начать с «Мне нужен ведущий»: приложение сразу покажет до трёх предварительных профилей по категории и спросит город и дату. Без даты доступность не подтверждается. Ответьте в том же поле; затем ИИ уточнит формат и бюджет, если их ещё нет. Когда все обязательные параметры известны, появится итоговый ранжированный подбор. Можно также ввести полный запрос одной фразой. Город и дату можно выбрать под полем: выбор сразу виден на кнопках и имеет приоритет над текстом. Строка «Я понял» показывает извлечённые условия. Свёрнутая форма «Изменить параметры или заполнить вручную» позволяет исправить их и остаётся доступной при сбое API. Готовых сценариев на странице нет; для показа жюри можно вводить следующие тексты.

СценарийТекст для поля поискаОжидаемый результат на исходном CSV
Плотная категорияНужен ведущий для корпоратива в Алматы 14 ноября 2026 года, бюджет до 1 500 000 тенге, русский язык, на 6 часов4 подходят, топ-3: HK-44923, HK-29829, HK-44733
Другая датаНужен ведущий для корпоратива в Алматы 15 ноября 2026 года, бюджет до 1 500 000 тенге, русский язык, на 6 часов6 подходят, топ-3: HK-88430, HK-44923, HK-35215
ПожеланиеНужен ведущий для корпоратива в Алматы 15 ноября 2026 года, бюджет до 1 500 000 тенге, русский язык, на 6 часов; важен опыт деловых форумовРезультат зависит от извлечённого пожелания; проверяйте строку «Я понял» и фразы в карточках
Редкая категорияНужен флорист для корпоратива в Алматы 10 октября 2026 года, бюджет до 300 000 тенге, русский язык, на 6 часов1 вариант: HK-39372; max_hours=null, цена восстановлена
Никто не проходитНужен ведущий для корпоратива в Алматы 14 ноября 2026 года, бюджет до 10 000 тенге, русский язык, на 6 часовКатегория есть, подходящих нет; 10 выше бюджета, 5 заняты, 1 не берёт формат; причины пересекаются
Категории нетНужен флорист для корпоратива в Зарубежье 14 ноября 2026 года, бюджет до 300 000 тенгеТакой категории в городе нет; отличается от отказа по условиям

Для короткого показа жюри введите запрос с пожеланием и покажите строку «Я понял», карточки и сравнение первых двух кандидатов. Затем измените дату текстом или через «Что меняется на другой дате» и покажите, кто вошёл и выбыл по календарю. После этого введите запросы для редкой категории и пустого результата. AI-сравнение описаний можно включить в раскрываемой форме: если API отвечает, появится проверенная дословная фраза и, когда сходство embeddings повышает оценку, её вклад. На 14 ноября заняты 5 ведущих Алматы, на 15 ноября — 2.

Повторить все сценарии без интерфейса:

.venv/bin/python -m matcher.demo

На Windows: .\.venv\Scripts\python.exe -m matcher.demo.

Архитектура и карта требований

Независимое локальное ядро: matcher/data.py и matcher/engine.py определяют допустимость, порядок и доказательства; matcher/explain.py формулирует факты. matcher/ai.py разбирает текст заявки через LLM, по запросу добавляет embeddings, выбирает дословные цитаты и проверяет их. matcher/intake.py проверяет извлечённые поля и составляет уточняющий вопрос. app.py отображает результаты и аудит. Жёсткие фильтры не обращаются к сети.

ТребованиеРеализация
CSV, списки через |, нормализация, ошибкиmatcher/data.py
Семь жёстких условий, три исхода, воронкаmatcher/engine.py
Прозрачные баллы, устойчивый порядокscore, select
Сопоставление пожелания с фразами описанияmatcher/text.py
Опциональные embeddings, LLM и проверка цитатmatcher/ai.py
Разбор свободного текста и уточненияmatcher/ai.py, matcher/intake.py
Индивидуальные объяснения, сравнение топ-1/топ-2 и оговоркиmatcher/explain.py
Текстовый поиск, ручная форма, карточки, отказы, сравнение дат, JSON-аудитapp.py; оформление — ui.css и .streamlit/config.toml
Воспроизводимые демоmatcher/demo.py
Проверки бизнес-логики, загрузки и формыtests/

Используются Python, Streamlit и стандартные csv, dataclasses, Decimal. Для 66 строк стандартный CSV-парсер позволяет явно контролировать пустые значения без автоматического превращения их в NaN. Pandas приходит как зависимость Streamlit для таблиц, но бизнес-логика от него не зависит. Точные денежные вычисления и сортировка выполняются с Decimal; округление применяется только для отображения.

Жёсткая фильтрация

  1. Точное совпадение выбранной категории с одним элементом categories. «Ведущий церемонии» не равен «Ведущий».
  2. Совпадение города. Пробелы, регистр, Unicode и ё/е нормализуются для сравнения.
  3. Календарь занятости заполнен, а дата отсутствует в busy_dates; разрешены только 23.09.2026–31.12.2026 включительно. Пустой busy_dates означает, что доступность неизвестна, поэтому профиль не выдаётся как свободный.
  4. price_from_kzt <= budget.
  5. Тип мероприятия присутствует в event_formats.
  6. Все выбранные пользователем языки присутствуют в languages.
  7. Если часы указаны и max_hours известен, hours <= max_hours.

max_hours=null означает, что ограничение неприменимо. Профиль проходит, но подтверждения длительности и баллов за этот фактор не получает. Неуказанные язык и часы не ограничивают поиск. Занятые профили никогда не попадают в рекомендации. Нулевая/отрицательная/нечисловая/бесконечная сумма бюджета возвращает понятную ошибку валидации; аналогично проверяются часы. В UI ноль часов означает «не указано», в ядре это None.

Три исхода явно различаются: found, no_category, no_matches. Если в городе вообще менее трёх профилей, это видно по счётчику; если часть отфильтрована, видны причины. Корректно подходящие ниже топ-3 не выдаются за отказавших по условиям.

Воронка последовательна: категория → город → доступность → бюджет → формат → язык → длительность. Отдельный подсчёт причин проверяет каждый профиль категории и города по всем условиям одновременно. Поэтому суммы причин могут быть больше числа исключённых профилей. В раскрываемой таблице можно проверить id каждого исключения.

Формула ранжирования

После фильтрации для каждого кандидата вычисляются факторы от 0 до 1:

ФакторБазовый весЗначение
Формат251: подтверждён обязательным фильтром
Язык201, если заданы и подтверждены все языки; без запроса фактор исключается
Бюджет201 − price_from_kzt / budget после обязательной проверки бюджета
Длительность151 при известном подходящем max_hours, 0 при null; без запроса фактор исключается
Пожелание20Явное количество языков проверяется по списку languages; остальные пожелания — по фразам описания и, в AI-режиме, через embeddings. Без пожелания фактор исключается

score = 100 × Σ(weight × factor) / Σ(active_weights).

При отсутствии необязательных параметров соответствующие веса исключаются и остальные нормируются к 100. В карточке всегда показаны все пять факторов: базовый вклад от 0 до фиксированного веса либо отметка, что параметр не задан и не участвует. При указанной длительности null сохраняет вес в знаменателе, но даёт 0. Оценки рассчитаны для конкретного запроса и не предназначены для сравнения разных запросов.

Бюджет — обязательный фильтр. После него меньшая цена «от» даёт больше баллов по цене. Это не рейтинг качества подрядчика; итоговую стоимость нужно уточнять.

Явное пожелание вроде «знание 3 языков» проверяется по структурированному списку языков профиля и получает полный вклад только при нужном количестве. Составное пожелание вроде «знание 3 языков и опыт фокусов» требует и нужного количества языков, и текстового подтверждения второй части; одна часть не даёт полный балл. Остальные пожелания локально сопоставляются с фразами описания по коротким основам слов; общие слова имеют меньший вес. Выбирается фраза с лучшим покрытием, она показывается дословно. Формат и язык не добавляют текстовых баллов повторно. Для пожелания с «без» засчитывается только фраза, где «без» тоже написано. AI-режим может распознать близкий смысл без совпадения слов, но начисляет AI-баллы только профилям с проверенной исходной фразой.

При равном точном score порядок определяется так:

  1. Больше подтверждений: формат + число запрошенных языков + известная подходящая длительность + число групп текста.
  2. Меньшая цена.
  3. Меньшая доля занятых дат в 100-дневном окне.
  4. Лексикографический порядок id.

Город, категория и доступность не входят в балл. Занятость используется как жёсткий фильтр, её доля — только как поздний tie-breaker по заданию. Порядок не зависит от порядка строк CSV и случайных чисел.

Объяснения и использование AI

Решение использует локальные правила и AI для ввода заявки. На 66 строках ничего не обучается. Свободный текст отправляется в OpenAI, при сбое — в NVIDIA. Модель возвращает структурированные поля; приложение проверяет типы, дату, положительный бюджет и формат из каталога. Если часть обязательных полей не указана, приложение спрашивает только о недостающих. Когда категория известна, оно показывает до трёх предварительных профилей по уже заданным условиям. Это ещё не итоговый рейтинг; без даты доступность не подтверждается. Итоговый ранжированный подбор запускается после заполнения всех обязательных полей. Подтверждённые поля можно исправить вручную. Разбор кешируется по тексту, предыдущим полям, модели, дате и составу каталога в игнорируемой Git папке .cache/; повторение одного запроса использует тот же разбор и тот же устойчивый порядок. В кеше хранится структурированный результат, а не исходный текст заявки.

Отдельный переключатель «Уточнить рекомендации с ИИ» включает смысловое сравнение пожелания с описаниями уже допустимых подрядчиков:

  1. Жёсткие фильтры отбирают допустимые профили.
  2. OpenAI LLM предлагает дословные фразы из их описаний. Приложение проверяет точное наличие фразы в исходном профиле.
  3. Второй вызов LLM проверяет смысловую связь фразы с пожеланием. Для установленного OpenAI-ключа сначала используется OpenAI; NVIDIA остаётся резервным провайдером. Неподтверждённая фраза не показывается, включая режим с одним ключом NVIDIA.
  4. OpenAI text-embedding-3-small сравнивает пожелание с описаниями профилей, у которых есть проверенная фраза. Большее из локального и AI-сходства используется в компоненте «Пожелание» с существующим весом 20. Остальные веса не меняются.
  5. При ошибке нужного API остаётся локальная оценка и шаблонное объяснение. Embeddings и проверенные цитаты кешируются локально в игнорируемой Git папке .cache/ по модели и хешу запроса/текста; повторный запрос их не пересчитывает. Сервис перечитывает кеш и конфигурацию при новом запуске подбора, поэтому результаты других процессов не остаются невидимыми до перезапуска приложения. Изменение правил аудита цитат сбрасывает старый кеш подтверждений.

После проверки извлечённых полей AI не добавляет кандидатов и не меняет цену, занятость или жёсткие условия. При одинаковом запросе и доступном режиме порядок определяется теми же числами и правилами tie-break; ответы LLM кешируются. При доступности только OpenAI AI-аудитор использует OpenAI. Ключ из brev.nvidia.com/settings/api-keys предназначен для Brev CLI и не заменяет ключ NVIDIA Developer API для NIM. Для NIM получите отдельный ключ на странице модели в build.nvidia.com и сохраните его как NVIDIA_API_KEY.

Первое предложение карточки называет дату, формат, цену и бюджет. Второе приводит исходную фразу профиля: в AI-режиме — выбранную LLM и пропущенную через аудит; иначе — найденную локальным поиском. Цитата обозначена как слова профиля, а не независимая оценка. Отдельный блок показывает точные условия, базовые баллы и ограничения. Оценка совпадения не является рейтингом качества подрядчика.

AI-сходство — эвристика, а не вероятность качества подрядчика. Если дословное подтверждение пожелания не найдено, локальный текстовый фактор остаётся без AI-надбавки. Сравнение дат в интерфейсе использует локальный стабильный порядок.

Данные и ограничения

В data/contractors.csv лежит неизменённая копия предоставленного CSV. Бизнес-требования прочитаны из PDF «HackAlem AI: Хакатон-задача: умный подбор подрядчиков». HTML-превью использовано для сверки: 66 уникальных id, множество полностью совпадает с CSV; заголовок подтверждает окно календаря.

Профилей 66, категорий 17 (один профиль может иметь несколько категорий). Городов: Алматы — 50, Астана — 15, Зарубежье — 1. Полностью синтетических профилей 13; восстановленных городов 8; восстановленных цен 18; max_hours отсутствует у 9. Флаги качества показываются на карточках и в JSON-аудите, но не дают бонусов. Имена анонимизированы. Синтетические профили не скрываются и участвуют по тем же правилам.

Занятость известна только на 100 дней. «Свободен» означает отсутствие даты в календаре датасета, не подтверждённую бронь. Профили, цены и описания не проверяются внешними источниками. Декабрьская загруженность и редкие категории закономерно создают пустые результаты.

Повреждённые обязательные поля, цены, часы, флаги, даты и дубли id исключаются с видимым отчётом. Для дублирующего id принимается только первая корректная встреченная запись с уникальным id; повтор помечается ошибкой. Пустое описание разрешено без смысловых баллов. Отсутствие колонок — понятная ошибка загрузки, а не пустая выдача. Пустая ячейка календаря, [] и null означают неизвестные данные: профиль остаётся в каталоге, но не выдаётся как доступный, а причина явно показывается пользователю. В исходном CSV пустых календарей нет. Если есть ошибки данных, UI сообщает, что результаты относятся к корректной части каталога.

Проверки

.venv/bin/python -m pytest -q --basetemp=.pytest_temp

На Windows используйте .\.venv\Scripts\python.exe -m pytest -q --basetemp=.pytest_temp. Тесты проверяют все жёсткие фильтры, три исхода, 1/2/3 результата, пустой календарь, бюджет, языки и длительность, влияние пожелания, ошибки CSV, границы календаря, уникальность объяснений, устойчивость порядка при перемешивании каталога и изменение выдачи по датам. AI-тесты без реальных API проверяют разбор текста, уточнение, кеш, резервного провайдера, дословность цитаты, аудит при одном ключе NVIDIA, совместимость с reasoning-моделью и неизменность жёстких фильтров. Streamlit AppTest проверяет первый экран, полный и неполный запрос, ручной ввод, сбой API и карточки результата. Документация тестового API: Streamlit AppTest.

Тесты проверяют также предварительную выдачу по короткому запросу, соблюдение уже известных ограничений и отсутствие преждевременной оценки или заявления о доступности. Проверен HTTP health endpoint и отображение страницы через Streamlit AppTest. API-пути покрыты тестами с имитацией ответов. Дополнительно живой AI-запрос «Мне нужен ведущий» дал три предварительных профиля и вопрос про город/дату; следующая реплика собрала полную заявку. Это единичные проверки, а не гарантия времени внешнего API. Ключи не входят в репозиторий.

Развитие

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

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

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

irridan123 9rava 6

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

  • 2026-09-23 17:58style: update prompt search button accessibility and visibilityirridan123
  • 2026-09-23 17:58feat: enhance landing page layout and prompt search functionality, update styles and add new assetsirridan123
  • 2026-09-23 17:42feat: enhance AI input handling and preliminary profile display in READMEirridan123
  • 2026-09-23 17:40feat: add quick filters for city and date selection, enhance request handlingirridan123
  • 2026-09-23 17:28feat: enhance preliminary profile previews and improve user prompts for better event request handlingrava
  • 2026-09-23 17:14style: update theme colors and enhance UI components for better user experienceirridan123
  • 2026-09-23 17:04Refactor app and AI integration for improved event request handlingirridan123
  • 2026-09-23 16:34fix: ground compound wishes and prepare public demorava
Все коммиты (15)
  • 2026-09-23 16:24feat: update AI model configurations and enhance UI componentsirridan123
  • 2026-09-23 15:54fix: match language count preferences to profile datarava
  • 2026-09-23 15:43feat: add grounded AI matching with audited evidencerava
  • 2026-09-23 15:28feat: improve explainable contractor matching demoirridan123
  • 2026-09-23 14:46feat: simplify UI and clarify recommendationsrava
  • 2026-09-23 14:22fix: stabilize Streamlit demo and availability checksirridan123
  • 2026-09-23 13:45feat: add explainable contractor matching demorava
Структура репозитория (25 файлов)
.env.example
.gitignore
.streamlit/config.toml
README.md
app.py
assets/botanical-panel.png
data/README.md
data/contractors.csv
matcher/__init__.py
matcher/ai.py
matcher/data.py
matcher/demo.py
matcher/engine.py
matcher/explain.py
matcher/intake.py
matcher/text.py
requirements.txt
run.ps1
run.sh
tests/test_ai.py
tests/test_app.py
tests/test_data.py
tests/test_engine.py
tests/test_intake.py
ui.css

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

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

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