AAYMA
RU
Войти

Точный подрядчик

Локальное приложение на чистом Python: рекомендует до 3 подрядчиков под событие по строгим фильтрам, объясняет выбор проверяемыми фактами из…

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

О проекте

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

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

Суть

Локальное приложение на чистом Python: рекомендует до 3 подрядчиков под событие по строгим фильтрам, объясняет выбор проверяемыми фактами из профилей и предлагает действия при пустой выдаче.

Как устроено

profiles.csv разбирается в списки. Жёсткие фильтры: точные город и категория, занятость даты, формат, бюджет относительно price_from_kzt, язык, длительность. Затем дополнительные баллы 0–100: +50/+35 за совпадение пожелания с description, +25 за формат, +10 за язык, до +15 за часы. Сортировка детерминированная, при равенстве по цене и id. Для пустых результатов определяется главная причина и предлагаются новые строгие запросы (бюджет, дата, город). OpenAI Responses API со structured outputs только выбирает ID уже проверенных фактов, текст собирается локально, а невалидный ответ отклоняется целиком.

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

  • hard-filter eligibility
  • additive match scoring
  • conservative stemming
  • date comparison re-run
  • structured outputs JSON Schema
  • ID-constrained LLM output

Особенности

Самый строгий анти-галлюцинационный контур: LLM возвращает только разрешённые ID фактов, а видимый текст собирается из проверенных значений. Нет внешних зависимостей. Честно указано, что живой вызов с ключом не проверялся, только mock-тестами. Для пустой выдачи предлагаются конкретные действия.

Стек

Python · HTML · CSS · JavaScript · unittest

Модели

OpenAI (model from OPENAI_MODEL)


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

README

README.md

MVP «Точный подрядчик» — кейс 79-lite

Локальное приложение рекомендует до трёх подрядчиков для события и объясняет каждую рекомендацию проверяемыми структурированными фактами. Интерфейс и сообщения — на русском языке.

Запуск

Нужен только Python 3.10+; внешних пакетов нет. Без API-ключа строгий подбор полностью работает, а блок AI честно показывает статус «недоступен».

python3 app.py

Открыть http://localhost:8000. Проверка здоровья: http://localhost:8000/health.

Тесты:

python3 -m unittest discover -s tests -v

Как устроен конвейер

  1. data/profiles.csv загружается при старте. Используются реальные поля: id, anon_name, categories, city, city_imputed, synthetic, price_from_kzt, price_imputed, event_formats, languages, max_hours, busy_dates, description.
  2. categories, event_formats, languages и busy_dates разбираются как списки с разделителем |. Категория совпадает только с целым элементом списка, без нечёткого поиска.
  3. Сначала выбираются точные город и категория. Затем как жёсткие условия проверяются занятость даты, формат, бюджет, язык и длительность. Бюджет сравнивается с price_from_kzt: это только начальная цена, поэтому карточка предупреждает, что итоговую стоимость нужно уточнить. Пустой max_hours означает, что ограничение длительности неприменимо. Площадки и их календари проходят тот же код, что и остальные подрядчики.
  4. Только после жёстких фильтров каждому допустимому профилю независимо рассчитываются дополнительные совпадения от 0 до 100. Формула: +50 за точное нормализованное совпадение пожелания с description, либо +35 за совпадение всех значимых корней пожелания в одном непублицистическом фрагменте; +25 за явное упоминание запрошенного формата в description; +10 за запрошенный язык в languages; при заданной длительности +10 за явный достаточный max_hours и ещё 0–5 за запас полных часов. Максимальная сумма — 100. Баллы не делятся между карточками, не являются вероятностью и не оценивают прохождение обязательных условий — оно показывается отдельным статусом.
  5. Профили сортируются детерминированно по убыванию суммы баллов. Только при равенстве используются цена по возрастанию и затем id. Цена не даёт баллов. Возвращаются первые три.
  6. Пожелания сопоставляются консервативно: нижний регистр, ё → е, удаление пунктуации, ограниченные корни слов и только две явные эквивалентности — «деловой/бизнес», а также формы слов вроде «форум/форумы» и «съёмка/съёмки». Совпадение не делает профиль допустимым и не может обойти ни один жёсткий фильтр.
  7. Не выбранные язык, длительность или пожелания дают 0 баллов без штрафа. Пустой max_hours проходит жёсткий фильтр, когда длительность задана, но не получает бонус за неизвестный запас. Цена и id используются только как детерминированные tie-breakers.
  8. В объяснениях различаются два вида фрагментов description. Балльные фрагменты подтверждают запрошенный формат или пожелание, входят в score_reasons и дают ровно те баллы, которые видны в раскрывающейся разбивке. Информационные фрагменты лишь сообщают конкретный факт о уже допустимом профиле: они не добавляют баллы, не меняют порядок и не обходят фильтры. Рекламные обещания, рейтинги и одно название коллектива не выдаются за преимущество. Если полезных индивидуальных сведений мало, карточка прямо сообщает об этом и перечисляет прошедшие проверяемые условия.
  9. Для сравнения дат весь конвейер запускается повторно. «Вошёл/вышел из топ-3» считается по двум рейтингам, а «стал свободен/занят» — отдельно и только прямой проверкой наличия каждой даты в busy_dates. Поэтому вытеснение из топ-3 более релевантным ставшим свободным профилем не называется новой занятостью.
  10. При пустом результате приложение определяет доминирующую причину понятным текстом. Оно показывает минимальную price_from_kzt среди профилей точных города и категории. Бюджетная кнопка предлагается только с минимальной начальной ценой кандидата, который уже проходит дату, формат и выбранные optional-ограничения; если эта сумма выше общего минимума категории, разница объясняется прямо в результате. Предлагаемые даты заранее проходят формат, бюджет, язык и длительность. Если язык или длительность блокируют выдачу, интерфейс может предложить убрать именно это ограничение. Если категории нет в городе, кнопка предлагает город, где такие профили действительно есть. Каждая кнопка меняет запрос и повторно запускает строгую фильтрацию — профили другого города не подмешиваются в исходную выдачу.

Безопасный AI-анализ

В инструкции HackAlem проверены материалы об активации выданных организаторами кредитов: провайдер — OpenAI. Конкретный ID модели и отдельный endpoint в материалах не зафиксированы. Поэтому приложение не угадывает модель: фактически используемая модель — ровно значение обязательной переменной OPENAI_MODEL. Запрос отправляется в официальный Responses API по https://api.openai.com/v1/responses; формат ответа задаётся через официальный Structured Outputs / JSON Schema.

Создайте локальный .env по примеру (он исключён из Git), укажите выданный ключ и точный доступный model ID, затем экспортируйте переменные перед запуском:

cp .env.example .env
# заполните OPENAI_API_KEY и OPENAI_MODEL в .env
set -a; source .env; set +a; python3 app.py

Можно также запускать одной командой без файла: OPENAI_API_KEY=... OPENAI_MODEL=... python3 app.py. Значения не попадают в HTML, /api/meta или ответ браузеру; заголовок авторизации создаётся только на сервере. Стандартный тайм-аут — 6 секунд, допустимый диапазон в коде ограничен 1–8 секундами.

AI запускается после Python-фильтров, расчёта баллов, сортировки и выбора первых трёх карточек. Ему передаются параметры запроса, только уже выбранные карточки, короткие разрешённые фрагменты/структурные факты и — для пустой выдачи — уже рассчитанные причины и действия. Полные календари и отброшенные профили не отправляются.

Модель не пишет пользовательский текст свободно. Она возвращает только структурированный выбор разрешённых ID. Сокращённый пример входа:

{
  "mode": "found",
  "request": {"city": "Алматы", "event_format": "корпоратив", "wishes": "деловой форум"},
  "cards": [
    {"id": "profile-17", "facts": [{"id": "profile-17:description:1", "text": "...проверенный фрагмент..."}], "wish_relation": "direct"}
  ]
}

Ожидаемый JSON модели:

{
  "mode": "found",
  "card_insights": [{"id": "profile-17", "fact_ids": ["profile-17:description:1"], "wish_fit": "direct"}],
  "reason_ids": [],
  "action_ids": []
}

Сервер повторно проверяет точный набор полей, режим ответа, порядок и полный набор ID карточек, принадлежность каждого fact_id своей карточке, связь с пожеланием и допустимость ID причин/действий. Видимый русский текст собирается локально только из проверенных значений. Выдуманный ID/факт, неверный JSON, ошибка API или тайм-аут отклоняются целиком: карточки не меняются, а интерфейс явно сообщает «AI-анализ недоступен».

В этой версии сетевой вызов с настоящим ключом не выполнен: в среде разработки нет активированных OPENAI_API_KEY и OPENAI_MODEL. Успешный ответ, невалидный JSON, выдуманный ID, тайм-аут и отсутствие конфигурации проверены mock-тестами. Следовательно, интеграция и защитный контур протестированы без списания организаторских кредитов, но доступ конкретного model ID должен быть подтверждён владельцем ключа.

Пользовательский интерфейс

  • На первом экране остаются пять обязательных полей: город, дата, формат, категория и бюджет. Алматы выбран по умолчанию как город с наибольшим числом профилей.
  • Категории, присутствующие в выбранном городе, показываются первыми. Остальные остаются видимыми и доступными в группе «Есть в других городах», поэтому валидные значения не исчезают.
  • Для бюджета есть быстрые варианты и ручной ввод. Язык, длительность, пожелания и сравнение дат находятся в закрытом по умолчанию разделе «Дополнительные параметры».
  • Краткий результат и практические действия видны сразу; независимые счётчики отказов скрыты под «Почему такой результат?».
  • Интерфейс адаптируется до одного столбца на телефоне. Ни одна клиентская кнопка не создаёт рекомендацию самостоятельно: источником результата всегда остаётся серверный recommend().

Основной подбор не является ML/LLM-системой: это небольшой детерминированный набор Python-правил, который полностью воспроизводится офлайн. LLM используется только для необязательного выбора наиболее полезных уже проверенных фактов в отдельном блоке и не может повлиять на допустимость, баллы, состав или порядок рекомендаций.

Три исхода поиска

  1. Рекомендации найдены (found) — показываются не более трёх профилей, прошедших все обязательные условия.
  2. В городе нет выбранной категории (no_city_category) — рекомендаций для исходного города нет; можно отдельной кнопкой изменить запрос на город, где категория представлена.
  3. Профили категории есть, но условия не выполнены (none_eligible) — рекомендации не показываются, интерфейс объясняет основную причину и предлагает только повторный строгий поиск с изменённым бюджетом, датой или optional-ограничением.

Воспроизводимая демонстрация

Все даты лежат в диапазоне CSV: 2026-09-23 — 2026-12-31. Бюджет ниже указан в KZT.

СценарийГородДатаФорматКатегорияБюджетОжидаемо
Плотная осенняя категорияАлматы2026-11-15корпоративВедущий100000007 проходят, показаны первые 3
Редкая категорияАстана2026-09-23свадьбаФото и видеобудки10000000ровно 1 рекомендация
Пустой результатАлматы2026-11-15свадьбаИнструменталист100000000; все 3 заняты
Бюджет ниже минимумаАлматы2026-09-23конференцияБанкетный зал10000; минимум категории 2000000, кнопка 3500000 для свободного профиля, проходящего остальные условия
Нет категории в городеАстана2026-09-23корпоративРесторан100000000; кнопка «Искать в городе Алматы · 7» меняет запрос и запускает поиск заново
Сравнение двух датАлматы2026-09-23свадьбаИнструменталист100000003 рекомендации; дата сравнения 2026-09-24 даёт 1
Пожелание меняет порядокАлматы2026-11-15корпоративВедущий10000000с пожеланием «ведущий для делового форума» Буллма выше более дешёвого Куррапики
Короткие описания ансамблейАлматы2026-12-26свадьбаНациональный ансамбль100000003 рекомендации; слабые или рекламные сведения помечены как недостаточные и не дают баллов

Для сценария «Сравнение двух дат» в поле «Сравнить с датой» укажите 2026-09-24: останется 1 рекомендация, интерфейс назовёт изменения и покажет, что Джет Блэк и Сакура Харуно стали заняты согласно busy_dates. Для полностью занятой даты 2026-11-15 интерфейс предлагает только даты, на которых строгий повторный расчёт находит доступных инструменталистов.

В плотном сценарии без пожеланий Куррапика получает 25 баллов за прямое подтверждение корпоративного формата. У остальных карточек description не влияет на баллы: конкретные фрагменты показаны только как информация о профиле. Рекламная фраза Кики про «топ-10» не используется.

Границы MVP

  • Только рекомендации: нет бронирования, заявок и уведомлений.
  • Доступность известна лишь для диапазона дат, представленного в busy_dates; запросы вне диапазона отклоняются.
  • price_from_kzt — начальная, а не окончательная стоимость. Фильтр гарантирует только то, что начальная цена не превышает бюджет; итоговую смету нужно уточнять у подрядчика.
  • Ранжирование сознательно простое и прозрачное. Текст влияет только на баллы уже допустимых профилей; он никогда не влияет на прохождение жёстких фильтров.
  • Счётчики причин отказа независимы и могут пересекаться: один профиль способен нарушить несколько условий.
  • Без ключа и точного OPENAI_MODEL отдельный AI-блок недоступен, но все рекомендации, объяснения карточек и действия пустого результата продолжают работать локально.
Звёзды
⭐ 0
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
11
Авторы
1
Файлы
13
Ветки
1

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

nurzhumabay 11

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

  • 2026-09-23 17:58Handle missing AI analysis response safelynurzhumabay
  • 2026-09-23 17:53Add safe structured AI analysisnurzhumabay
  • 2026-09-23 17:30Clarify evidence limits in recommendation explanationsnurzhumabay
  • 2026-09-23 17:19Use distinct description evidence in explanationsnurzhumabay
  • 2026-09-23 17:03Document final verification scenariosnurzhumabay
  • 2026-09-23 16:56Clarify starting prices and city suggestionsnurzhumabay
  • 2026-09-23 16:49Clarify eligibility and additional match scoresnurzhumabay
  • 2026-09-23 16:37Differentiate contractor evidence in explanationsnurzhumabay
Все коммиты (11)
  • 2026-09-23 16:20Refine scoring and actionable empty resultsnurzhumabay
  • 2026-09-23 15:51Polish customer search and empty statesnurzhumabay
  • 2026-09-23 14:46Build explainable contractor recommendation MVPnurzhumabay
Структура репозитория (13 файлов)
.env.example
.gitignore
README.md
app.py
app/__init__.py
app/ai_analysis.py
app/recommender.py
data/profiles.csv
static/app.js
static/index.html
static/styles.css
tests/test_ai_analysis.py
tests/test_recommender.py

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

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

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