Tandau
Сервис подбора до трёх event-подрядчиков по городу, дате, категории, формату, бюджету, языку и длительности, с объяснениями и диагностикой причин…
О проекте
Команда: Vision2026
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Сервис подбора до трёх event-подрядчиков по городу, дате, категории, формату, бюджету, языку и длительности, с объяснениями и диагностикой причин исключения.
Как устроено
CSV из 66 профилей загружается в память. Жёсткие фильтры исключают занятых по календарю, дороже бюджета, без нужного формата, языка или часов. Прошедшие ранжируются детерминированно (упоминание формата в описании → цена «от» → id). Для объяснения описание делится на фрагменты, OpenAI возвращает номера подходящих фрагментов, код проверяет их и вставляет исходный текст профиля. При ошибке API объяснение строится по правилам. Различаются исходы matched / category_absent / no_match. Доступ через FastAPI и Streamlit.
Ключевые решения
- жёсткие фильтры с подсчётом причин исключения
- стабильное лексикографическое ранжирование
- экстрактивный выбор фрагментов LLM с валидацией индексов
- кэш ответов (128)
Особенности
LLM не генерирует текст, а выбирает номер фрагмента исходного описания, поэтому выдуманные биографии исключены. Порядок карточек не зависит от модели.
Стек
Python · FastAPI · Pydantic · Uvicorn · Streamlit · unittest
Модели
gpt-4.1-mini (OpenAI Responses API)
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-74a0463d-vision2026.
README
README.mdTandau - Умный подбор подрядчиков
Сервис рекомендаций event-подрядчиков для мероприятий в Казахстане. Разработан в рамках HackAlem AI / Vision2026, задача #79-lite.
Пользователь задаёт параметры мероприятия и получает до трёх подходящих подрядчиков с конкретными объяснениями. Сервис учитывает занятость, бюджет, формат, язык и длительность, а при отсутствии результатов сообщает причину.
Возможности
- Подбор по городу, дате, категории подрядчика и формату мероприятия.
- Проверка бюджета, языка и длительности.
- Исключение занятых подрядчиков, включая площадки.
- До трёх карточек с именем, категорией, городом, ценой «от» и объяснением.
- Стабильный порядок результатов при одинаковых параметрах и данных.
- Выбор фактов из описаний с помощью OpenAI API.
- Работа с объяснениями по правилам при недоступности AI.
- Отдельная обработка отсутствующей категории и неподходящих условий.
- Сохранение признаков синтетических и подставленных данных.
Текущая версия включает Streamlit-интерфейс, backend и интерактивную документацию Swagger UI. Бронирование, оплата и уведомления подрядчикам не входят в функциональность.
Технологии
| Технология | Назначение |
|---|---|
| Python | Обработка данных и логика подбора |
| FastAPI | HTTP API и интерактивная документация |
| Pydantic | Проверка входных параметров |
| Uvicorn | Запуск сервера |
| OpenAI API | Выбор полезных фактов из описаний |
| python-dotenv | Загрузка конфигурации |
| unittest | Автоматические тесты |
Запуск
Требуется Python 3.10+. Команды ниже приведены для PowerShell.
Установка
git clone https://github.com/BAITC-Hacks/hack-74a0463d-vision2026.git
cd hack-74a0463d-vision2026
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Пакет Streamlit для интерфейса устанавливается вместе с остальными зависимостями из requirements.txt.
Для закрытого репозитория необходим доступ к организации или проекту.
Конфигурация
В корне проекта используется файл .env:
OPENAI_API_KEY=your_api_key
OPENAI_MODEL=gpt-4.1-mini
API-ключ должен иметь разрешение на создание ответов через /v1/responses. Выбранная модель должна быть доступна в проекте OpenAI.
Файл .env исключён из Git. Ключ используется только на стороне backend.
Без ключа или при ошибке API сервис возвращает объяснения по правилам.
Запуск сервера
.\.venv\Scripts\python.exe -m uvicorn backend.app:app --reload
После запуска доступны:
- Swagger UI: http://127.0.0.1:8000/docs
- Проверка сервера: http://127.0.0.1:8000/health
- Параметры формы: http://127.0.0.1:8000/options
Для исходного датасета /health возвращает:
{
"status": "ok",
"profiles": 66
}
В macOS и Linux вместо ..venv\Scripts\python.exe используется .venv/bin/python.
Запуск интерфейса Streamlit
Из корневой папки проекта запустите интерфейс отдельной командой:
.\.venv\Scripts\python.exe -m streamlit run frontend/streamlit_app.py
Откройте адрес, который выведет Streamlit в терминале; обычно это http://localhost:8501. Для интерфейса отдельный запуск Uvicorn не требуется: Streamlit загружает каталог и вызывает модули backend напрямую.
API
| Метод | Endpoint | Назначение |
|---|---|---|
| GET | /health | Состояние сервера и количество профилей |
| GET | /options | Города, категории, форматы, языки и границы календаря |
| POST | /recommend | Подбор подрядчиков |
Запрос на подбор
Заголовок: Content-Type: application/json.
{
"city": "Алматы",
"date": "2026-10-15",
"event_format": "корпоратив",
"category": "Ведущий",
"budget_kzt": 2000000,
"duration_hours": 5,
"language": "русский"
}
| Поле | Обязательное | Описание |
|---|---|---|
| city | Да | Город |
| date | Да | Дата в формате YYYY-MM-DD |
event_format | Да | Формат мероприятия |
| category | Да | Категория подрядчика |
budget_kzt | Да | Положительный бюджет в тенге |
duration_hours | Нет | Положительная длительность в часах |
| language | Нет | Требуемый язык |
Бюджет относится к выбранному подрядчику, а не ко всему мероприятию. Необязательные поля можно пропустить или передать как null.
Ответ
Ответ содержит:
status— результат подбора;cards— список до трёх карточек;message— пояснение результата;diagnostics— количество подходящих и исключённых профилей, причины исключения.
Карточка содержит:
id
anon_name
category
city
price_from_kzt
explanation
synthetic
city_imputed
price_imputed
explanation_source
Три исхода подбора
| Статус | Значение |
|---|---|
matched | Найдены подходящие подрядчики |
category_absent | В каталоге нет выбранной категории в указанном городе |
no_match | Категория есть, но ни один профиль не соответствует условиям |
Если подходят один или два кандидата, возвращаются только они. Причина короткой выдачи указывается в message.
Все три исхода возвращаются с HTTP 200. Некорректный ввод получает HTTP 422 с описанием ошибки в поле detail.
Как работает подбор
1. Подготовка данных
CSV загружается в память при запуске сервера.
Загрузчик преобразует:
- категории, форматы и языки — в списки;
- занятые даты — в множество дат;
- цены и длительность — в числа;
- признаки синтетических данных — в логические значения.
Также проверяются идентификаторы, цены и длительность.
2. Фильтрация
Сначала выбираются профили нужного города и категории. Затем исключаются подрядчики, которые:
- заняты на указанную дату;
- имеют цену «от» выше бюджета;
- не поддерживают выбранный формат;
- не работают на указанном языке;
- не могут работать необходимое количество часов.
Площадки проходят такую же проверку календаря, как ведущие и фотографы.
Пустой max_hours означает, что ограничение по присутствию неприменимо. Он не считается нулём.
У профиля может быть несколько причин исключения. Поэтому сумма счётчиков причин может превышать количество исключённых подрядчиков.
3. Ранжирование
Прошедшие фильтры кандидаты сортируются последовательно:
- Наличие прямого упоминания формата мероприятия в описании.
- Меньшая цена «от».
- Идентификатор id при равенстве предыдущих условий.
Возвращаются первые три профиля. Сортировка выполняется кодом и не зависит от ответа модели.
4. Объяснение рекомендации
Объяснение объединяет соответствие условиям запроса и конкретную деталь из описания подрядчика.
AI используется для выбора этой детали. Описание разбивается на фрагменты, после чего модель получает параметры запроса и фрагменты выбранных профилей.
Модель возвращает номера фрагментов, а код проверяет идентификаторы и допустимость номеров. В карточку вставляется исходный текст профиля.
Таким образом, AI не изменяет цены, календарь, состав или порядок выдачи и не добавляет свободно сгенерированную биографию.
Надёжность AI-компонента
- На выбранные карточки выполняется один запрос к модели.
- Для пустого результата модель не вызывается.
- Успешные результаты сохраняются в памяти процесса, до 128 вариантов.
- При ошибке API или некорректном ответе используются объяснения по правилам.
- Для обращения к модели задан таймаут 8 секунд без повторных попыток.
Поле explanation_source показывает происхождение объяснения:
- ai — факт выбрала модель;
- rules — использовано объяснение по правилам.
Кэш очищается при перезапуске. После перезапуска выбранный AI-фрагмент может измениться, но порядок карточек остаётся стабильным.
Проверка ответа модели предотвращает вставку несуществующего фрагмента, однако не гарантирует полезность выбранной цитаты или достоверность исходного профиля.
Данные
Используется анонимизированный датасет организаторов:
- 66 профилей;
- 13 синтетических профилей;
- города: Алматы, Астана, Зарубежье;
- календарь: 23 сентября — 31 декабря 2026 года.
Файл расположен в backend/data/contactors.csv.
Особенности:
price_from_kzt— цена «от» за мероприятие, а не окончательная стоимость или почасовой тариф;syntheticотмечает синтетические профили;city_imputedиprice_imputedпоказывают, что значения были проставлены при подготовке датасета;- запросы вне доступного календаря отклоняются;
- высокая занятость в декабре является особенностью исходных данных.
Демонстрационные сценарии
Сценарии выполняются через POST /recommend на странице /docs. Ожидаемые результаты соответствуют исходному CSV и текущим правилам ранжирования.
Массовая категория
Для запроса из раздела API подходят 7 ведущих, возвращаются:
- Мицури Канроджи — HK-44923.
- Аня Форджер — HK-29829.
- Кики — HK-35215.
Повторный запрос сохраняет порядок.
Изменение даты
В том же запросе дата меняется на 2026-10-16.
Подходят 5 профилей, возвращаются:
- Куррапика — HK-88430.
- Мицури Канроджи — HK-44923.
- Буллма — HK-44733.
Куррапика занят 15 октября и свободен по календарю 16 октября. Аня Форджер и Кики, напротив, заняты 16 октября. Причины исключения отражаются в ответе.
Редкая категория
{
"city": "Алматы",
"date": "2026-10-15",
"event_format": "корпоратив",
"category": "Флорист",
"budget_kzt": 300000
}
Возвращается одна карточка: Тони Тони Чоппер — HK-39372.
В этой категории в Алматы два профиля. Второй не поддерживает выбранный формат. Сервис объясняет, почему результатов меньше трёх.
Кандидаты есть, но условия не подходят
В базовом запросе ведущего бюджет меняется на 1.
Ожидаемый результат: no_match, пустой список карточек и сообщение о превышении бюджета.
Категория отсутствует в городе
В базовом запросе город меняется на Астана, категория — на Декоратор.
Ожидаемый результат: category_absent и сообщение об отсутствии категории в городе.
Синтетические профили
{
"city": "Алматы",
"date": "2026-10-15",
"event_format": "корпоратив",
"category": "Фото и видеобудки",
"budget_kzt": 500000
}
Возвращаются:
- HK-90009 —
synthetic: true; - HK-35846 —
synthetic: false.
Тестирование
.\.venv\Scripts\python.exe -m unittest backend.test_recommender -v
Набор содержит 11 тестов, проверяющих:
- граничные значения бюджета и длительности;
- занятость людей и площадок;
- исключение по каждому обязательному условию;
- отсутствие категории в городе;
- короткую выдачу и ограничение в три карточки;
- стабильность порядка;
- влияние даты;
- необязательные поля и пустую длительность;
- некорректный ввод;
- использование исходных фактов;
- обработку ошибок AI.
Тесты используют отдельные тестовые профили и имитацию API. Они не меняют исходный CSV и не расходуют квоту OpenAI.
Ожидаемое завершение:
Ran 11 tests in ...
OK
Структура проекта
.
├── backend/
│ ├── data/
│ │ └── contactors.csv
│ ├── loader.py
│ ├── recommender.py
│ ├── ai_client.py
│ ├── ai_explanations.py
│ ├── app.py
│ └── test_recommender.py
├── frontend/
│ ├── demo_queries.py
│ ├── streamlit_app.py
│ └── ui_components.py
├── requirements.txt
├── .gitignore
└── README.md
Фронтенд Streamlit
Интерфейс написан на Streamlit. Точка входа — frontend/streamlit_app.py; оформление и карточки вынесены в frontend/ui_components.py, а демонстрационные запросы — в frontend/demo_queries.py.
Форма позволяет выбрать город, категорию, формат и дату мероприятия, задать бюджет, а также при желании указать длительность и язык общения. Списки городов и категорий формируются из загруженного CSV-каталога.
- Три готовых демонстрационных сценария заполняют форму для популярной категории, редкой категории и запроса без подходящих вариантов.
- После отправки формы приложение вызывает
backend.recommender.recommendнапрямую; каталог загружается черезbackend.loader.load_contractorsи кэшируется средствами Streamlit. - Интерфейс показывает до трёх карточек с именем профиля, категорией, городом, ценой «от» и индивидуальным объяснением рекомендации.
- Синтетические профили и восстановленные значения города или цены отмечаются на карточке отдельными бейджами.
- Для пустой выдачи показываются сообщение backend и счётчики основных причин, мешавших подбору; при отсутствии категории в городе отображается отдельное сообщение. Интерфейс оформлен в светлой палитре с лиловыми акцентами, округлыми карточками и фиолетовой основной кнопкой без декоративного эмодзи. Предусмотрены состояния загрузки и ошибки.
Текущая страница Streamlit не отправляет HTTP-запросы на GET /options или POST /recommend и не использует CORS: она вызывает Python-функции backend напрямую. Эти HTTP-маршруты остаются доступными для отдельной проверки API. При раздельном размещении интерфейса и backend потребуется настроить адрес сервера и CORS.
Ограничения
- Каталог и календари статичны.
- Ранжирование основано на правилах; отзывы и подтверждённые оценки качества не используются.
- AI может выбрать слишком общий фрагмент. Конкретность, различимость и длина объяснений требуют ручной оценки.
- Целевое время ответа — до 10 секунд. Измерения полного времени ответа пока не опубликованы; таймаут API не является гарантией общей задержки.
- Версии большинства зависимостей в
requirements.txtне закреплены. - Текущие инструкции описывают локальную демонстрацию Streamlit-интерфейса и backend. Публичная deployed-версия в проекте не указана.
Развитие
- Оценка качества рекомендаций на примерах, размеченных экспертами.
- Улучшение выбора фактов и ранжирования.
- Учёт пожеланий к стилю мероприятия.
- Подключение обновляемых каталогов и календарей.
- Измерение времени выбора и полезности рекомендаций для пользователей.
- Переход к базе данных и оценка семантического поиска при расширении каталога.
Авторы коммитов
История коммитов
- 2026-09-23 17:57Add README.md with service details and setup instructionsakmaralabilhayir-lab
- 2026-09-23 17:20(feat):изменение дизайнаwassuprizu
- 2026-09-23 17:17(feat):изменение дзиайнwassuprizu
- 2026-09-23 16:14Add Streamlit frontend for contractor recommendationswassuprizu
- 2026-09-23 15:29test: проверить фильтры, порядок и обработку ошибок AIakmaralabilhayir-lab
- 2026-09-23 15:16feat: добавить API подбора подрядчиков и параметры формыakmaralabilhayir-lab
- 2026-09-23 15:03feat: подключить AI к выбору фактов для объясненийakmaralabilhayir-lab
- 2026-09-23 14:53feat: подключить OpenAI API и добавить проверкуakmaralabilhayir-lab
Все коммиты (13)
- 2026-09-23 14:05feat: добавить стабильное ранжирование и базовые объясненияakmaralabilhayir-lab
- 2026-09-23 13:55feat: добавить фильтрацию подрядчиков и причины отказаakmaralabilhayir-lab
- 2026-09-23 13:39feat: добавить загрузку и проверку данных подрядчиковakmaralabilhayir-lab
- 2026-09-23 13:37data: добавить исходный датасет подрядчиковakmaralabilhayir-lab
- 2026-09-23 13:29chore: добавить структуру проекта и датасетakmaralabilhayir-lab
Структура репозитория (13 файлов)
.gitignore README.md backend/ai_client.py backend/ai_explanations.py backend/app.py backend/data/contactors.csv backend/loader.py backend/recommender.py backend/test_recommender.py frontend/demo_queries.py frontend/streamlit_app.py frontend/ui_components.py requirements.txt
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.