Shabyt
Веб-сервис подбора до трёх event-подрядчиков по городу, дате, типу события, категории и бюджету с объяснением каждого выбора.
О проекте
Команда: Triple-A
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Веб-сервис подбора до трёх event-подрядчиков по городу, дате, типу события, категории и бюджету с объяснением каждого выбора.
Как устроено
FastAPI загружает JSONL-каталог (66 профилей + 3 синтетических), filters.py отсекает занятых/не укладывающихся в бюджет/формат/язык/длительность, ranking.py детерминированно ранжирует по близости цены к бюджету снизу, бонусам за язык и ключевые слова, tie-break по id. Для топ-3 llm_explain.py генерирует объяснение через OpenAI gpt-4o-mini, иначе локальный шаблонный fallback; различаются состояния ok / no_category / no_match. Есть статический режим для GitHub Pages.
Ключевые решения
- жёсткие фильтры
- детерминированное скоринговое ранжирование
- LLM-объяснения с fallback
Особенности
Разделение пустой выдачи на no_category и no_match с причинами исключения; статический клиентский режим поиска для GitHub Pages; сохранённые QA-артефакты (90 backend-тестов, 203 core-проверки).
Стек
Python · FastAPI · Pydantic · Uvicorn · React 19 · TypeScript · Vite · Vitest · pytest · GitHub Actions
Модели
gpt-4o-mini
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-e68c7bd2-triple-a.
README
README.mdShabyt — умный подбор подрядчиков для мероприятий
Решение для кейса HackAlem AI «Умный подбор подрядчиков (#79-lite)».
Shabyt помогает заказчику выбрать подходящего event-подрядчика из каталога по конкретным условиям мероприятия: городу, дате, типу события, категории и бюджету. Дополнительно можно учитывать язык и длительность работы.
Сервис не просто выводит каталог, а фильтрует кандидатов, детерминированно ранжирует подходящих и возвращает до 3 карточек с конкретным объяснением, почему каждый подрядчик попал в выдачу.
1. Проблема и решение
При выборе подрядчиков на мероприятие пользователь сталкивается с большим количеством профилей, но не всегда понимает:
- кто работает в нужном городе;
- кто свободен на конкретную дату;
- кто подходит по бюджету;
- кто берёт нужный формат мероприятия;
- кто работает на нужном языке и нужное количество часов;
- почему один подходящий профиль оказался выше другого.
Shabyt автоматизирует этот отбор. Пользователь задаёт параметры мероприятия, после чего система возвращает до трёх наиболее подходящих профилей и объясняет подбор на основе данных каталога.
2. Что реализовано
Поиск и фильтрация
Система учитывает:
- город;
- дату мероприятия;
- тип мероприятия;
- категорию подрядчика;
- бюджет;
- язык — опционально;
- длительность — опционально.
Подрядчик исключается из выдачи, если он:
- занят на выбранную дату;
- не укладывается в бюджет;
- не работает с выбранным форматом мероприятия;
- не поддерживает выбранный язык;
- не подходит по длительности работы.
Детерминированное ранжирование
После фильтрации кандидаты ранжируются без ML/LLM по воспроизводимым правилам:
- чем ближе цена
price_from_kztк бюджету снизу, тем выше кандидат; - добавляется бонус за совпадение языка;
- добавляется бонус за релевантные слова запроса в описании профиля;
- при одинаковом score используется сортировка по
id.
Одинаковый запрос даёт одинаковый порядок карточек.
Три отдельных результата поиска
API и интерфейс различают состояния:
ok— найдены подходящие подрядчики;no_category— в выбранном городе нет такой категории;no_match— категория есть, но никто не прошёл условия запроса.
Если подходят только один или два профиля, сервис возвращает их и объясняет, почему результатов меньше трёх.
Объяснения рекомендаций
Для каждой карточки формируется краткое фактическое объяснение.
Есть два режима:
- локальный детерминированный режим — работает без API-ключа;
- OpenAI — если задан
OPENAI_API_KEY, backend запрашивает LLM-объяснение. Модель по умолчанию задаётся какgpt-4o-miniчерезOPENAI_EXPLAIN_MODEL.
Если внешний LLM недоступен или не отвечает вовремя, сервис возвращает локальное объяснение вместо падения запроса.
Frontend
Реализован адаптивный интерфейс на React + TypeScript:
- форма поиска;
- выбор всех категорий каталога;
- демо-пресеты для быстрой проверки;
- состояния загрузки и ошибок;
- отдельные состояния пустой выдачи;
- карточки результатов;
- сортировка результатов в интерфейсе;
- раскрытие деталей профиля;
- светлая/тёмная тема;
- мобильная адаптация.
Проверки
В репозитории есть:
backend/tests/— pytest-тесты backend;- Vitest-тесты frontend;
frontend/scripts/check-core.cjs— дополнительные проверки логики;CHECK_WINDOWS.bat— единая команда проверки перед демонстрацией;QA_REPORT.mdи папкаqa/с сохранёнными результатами предыдущих прогонов.
Сохранённый в репозитории backend-лог содержит 90 passed, а core-проверки frontend — 203 core checks passed. Перед финальной сдачей рекомендуется повторить CHECK_WINDOWS.bat на машине команды.
3. Как работает решение
Основной пользовательский сценарий:
- Пользователь открывает Shabyt.
- Выбирает город, дату, тип мероприятия, категорию и бюджет.
- При необходимости указывает язык и длительность.
- Frontend отправляет
POST /api/search. - Backend загружает каталог подрядчиков.
filters.pyотбрасывает неподходящие профили.ranking.pyдетерминированно ранжирует прошедших фильтр кандидатов.- Для топ-3 формируется объяснение через OpenAI либо локальный fallback.
- Frontend показывает до трёх карточек и объяснение результата.
Если подходящих кандидатов нет, пользователь получает понятную причину, а не пустой экран.
4. Технологии
Frontend
- React 19
- TypeScript
- Vite 8
- CSS
- Manrope (
@fontsource/manrope) - Vitest
- Testing Library
Backend
- Python 3.11+
- FastAPI
- Pydantic 2
- Uvicorn
python-dotenv- OpenAI Python SDK
- pytest
AI / внешние сервисы
- OpenAI API — опциональная генерация объяснений рекомендаций.
- Модель по умолчанию:
gpt-4o-mini. - Без OpenAI API проект продолжает работать в локальном rule-based режиме.
- NVIDIA API в текущей версии репозитория не интегрирован.
5. Архитектура
┌─────────────────────────────┐
│ React / TypeScript │
│ Vite frontend │
└──────────────┬──────────────┘
│ POST /api/search
▼
┌─────────────────────────────┐
│ FastAPI │
│ main.py │
└──────────────┬──────────────┘
│
┌───────┴────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ data_loader │ │ validation │
└──────┬──────┘ └─────────────┘
▼
┌─────────────┐
│ filters.py │
└──────┬──────┘
▼
┌─────────────┐
│ ranking.py │
└──────┬──────┘
▼
┌─────────────────────────────┐
│ llm_explain.py │
│ OpenAI API / local fallback │
└──────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ До 3 карточек + reason │
└─────────────────────────────┘
Основные директории
backend/
main.py FastAPI, схемы запроса/ответа, endpoints
data_loader.py загрузка и проверка JSONL
filters.py фильтрация кандидатов
ranking.py детерминированное ранжирование
llm_explain.py объяснения OpenAI / fallback
data/ основной датасет
synthetic_profiles.jsonl дополнительные synthetic-профили
tests/ backend-тесты
frontend/
src/components/ UI-компоненты
src/services/ API и статический поиск
src/hooks/ состояние и запросы
src/styles/ стили
public/data/ каталог для статического режима
qa/ сохранённые QA-артефакты
Режим GitHub Pages
В проекте присутствует GitHub Actions workflow .github/workflows/pages.yml.
Для Pages предусмотрен отдельный статический режим каталога: frontend может выполнять поиск по frontend/public/data/contractors.json без запущенного FastAPI. Обычный локальный режим использует backend API.
6. Данные
Используется предоставленный организаторами анонимизированный каталог event-подрядчиков.
Основной файл:
backend/data/hackathon-dataset-anonymized.jsonl
В нём 66 профилей.
Дополнительно проект загружает:
backend/synthetic_profiles.jsonl
В нём 3 дополнительных synthetic-профиля для демонстрации редких категорий.
Итого backend загружает 69 уникальных профилей, из которых 16 имеют synthetic: true с учётом synthetic-профилей, уже присутствующих в исходном датасете.
Каталог содержит 17 категорий и города:
- Алматы;
- Астана;
- Зарубежье.
Календарь занятости ограничен диапазоном:
23.09.2026 — 31.12.2026
Frontend содержит эквивалентный каталог frontend/public/data/contractors.json для статического режима.
7. Установка и запуск
Требования
- Python 3.11+
- Node.js ^20.19.0 или >=22.12.0
- npm
API-ключ OpenAI не обязателен для запуска.
Самый простой запуск на Windows
Из корня проекта:
.\START_WINDOWS.bat
Скрипт:
- создаст
backend/.venv; - установит Python-зависимости;
- установит frontend-зависимости через
npm ci; - выполнит production build frontend;
- запустит FastAPI.
После запуска открыть:
http://127.0.0.1:8000/
Swagger:
http://127.0.0.1:8000/docs
Health check:
http://127.0.0.1:8000/health
Режим разработки
Backend
Из корня проекта:
python -m venv backend\.venv
backend\.venv\Scripts\python.exe -m pip install -r backend\requirements.txt
backend\.venv\Scripts\python.exe -m uvicorn main:app --app-dir backend --reload --host 127.0.0.1 --port 8000
Frontend
Во втором терминале:
cd frontend
npm ci --include=dev
npm run dev
Frontend:
http://127.0.0.1:4173/
Backend API:
http://127.0.0.1:8000/
8. Подключение OpenAI
Скопируйте:
backend/.env.example
в:
backend/.env
и укажите:
OPENAI_API_KEY=your_key_here
OPENAI_EXPLAIN_MODEL=gpt-4o-mini
После этого перезапустите backend.
Не публикуйте .env и API-ключи в GitHub.
Если OPENAI_API_KEY отсутствует, используются локальные объяснения.
9. Как проверить решение
Вариант 1 — через интерфейс
После запуска откройте сайт и используйте демо-пресеты над формой либо заполните форму вручную.
Сценарий A — обычная выдача
Город: Алматы
Дата: 14.11.2026
Тип: Свадьба
Категория: Фотограф
Бюджет: 300 000 ₸
Ожидаемый результат: ok, до 3 карточек. В текущем каталоге этот запрос возвращает три профиля.
Сценарий B — редкая категория
Город: Алматы
Дата: 14.11.2026
Тип: Свадьба
Категория: Флорист
Бюджет: 300 000 ₸
Ожидаемый результат: ok, один подходящий профиль и сообщение, почему карточек меньше трёх. Synthetic-профиль помечается соответствующим флагом.
Сценарий C — кандидаты есть, но не проходят условия
Город: Алматы
Дата: 14.11.2026
Тип: Свадьба
Категория: Фотограф
Бюджет: 1 000 ₸
Ожидаемый результат: no_match с причинами исключения кандидатов.
Сценарий D — категории нет в городе
Город: Астана
Дата: 14.11.2026
Тип: Свадьба
Категория: Лайв-бэнд
Бюджет: 2 000 000 ₸
Ожидаемый результат: no_category.
Вариант 2 — через Swagger
Откройте:
http://127.0.0.1:8000/docs
и выполните POST /search:
{
"city": "Алматы",
"event_date": "2026-11-14",
"event_type": "свадьба",
"category": "Фотограф",
"budget_kzt": 300000,
"duration_hours": null,
"language": null
}
Автоматические проверки
На Windows:
.\CHECK_WINDOWS.bat
Команда запускает backend-тесты, frontend core checks, Vitest и production build.
10. API
POST /search
Также доступен alias:
POST /api/search
Пример запроса:
{
"city": "Алматы",
"event_date": "2026-11-14",
"event_type": "свадьба",
"category": "Фотограф",
"budget_kzt": 300000,
"duration_hours": null,
"language": null
}
Пример структуры ответа:
{
"status": "ok",
"message": null,
"results": [
{
"id": "...",
"name": "...",
"category": "Фотограф",
"city": "Алматы",
"price_from_kzt": 300000,
"explanation": "...",
"synthetic": false
}
]
}
Дополнительные endpoints
GET /health
GET /catalog/meta
Для frontend также зарегистрированы /api/health и /api/catalog/meta.
11. Ограничения текущей версии
- Проект не выполняет бронирование подрядчиков.
- Нет отправки заявок и уведомлений подрядчику.
- Нет пользовательской регистрации и авторизации.
- Нет контактов подрядчиков — используется анонимизированный хакатон-датасет.
price_from_kzt— цена «от», а не финальная смета.- Доступность определяется только календарём датасета, а не реальной системой бронирования.
- Поиск по датам доступен только для 23.09.2026–31.12.2026.
- OpenAI используется только для объяснений и является опциональным; без ключа работает rule-based fallback.
- NVIDIA API в текущем коде не подключён.
- Статический GitHub Pages-режим работает без backend и поэтому использует локальную логику и локальные объяснения.
12. Развёрнутая версия
В репозитории настроен workflow для GitHub Pages, однако конкретный публичный URL развёрнутой версии не зафиксирован в исходниках проекта.
Если Pages уже успешно опубликован, добавьте сюда URL перед сдачей:
Deployed: <вставьте публичную ссылку>
Репозиторий
https://github.com/BAITC-Hacks/hack-e68c7bd2-triple-a
Безопасность
Не коммитьте в GitHub:
backend/.env;OPENAI_API_KEY;- любые другие секреты и API-токены.
Для примера конфигурации используйте только backend/.env.example.
Авторы коммитов
История коммитов
- 2026-09-23 17:54Update README.mdpoterpish
- 2026-09-23 17:45Add files via uploadmustaf1nx
- 2026-09-23 17:35Add files via uploadmustaf1nx
- 2026-09-23 17:34Add files via uploadmustaf1nx
- 2026-09-23 17:33Add files via uploadmustaf1nx
- 2026-09-23 17:32Delete shabyt-design-package directorymustaf1nx
- 2026-09-23 17:32Delete frontend directorymustaf1nx
- 2026-09-23 17:32Delete backend directorymustaf1nx
Все коммиты (16)
- 2026-09-23 16:33README.mdpoterpish
- 2026-09-23 16:26frontend: rebrand as Shabyt and add GitHub Pages deploymentANAYSSA
- 2026-09-23 16:19Add files via uploadmustaf1nx
- 2026-09-23 16:18Delete backend.zipmustaf1nx
- 2026-09-23 16:18Add files via uploadmustaf1nx
- 2026-09-23 15:50frontend: refactor into tested React TypeScript applicationANAYSSA
- 2026-09-23 15:14frontend: add responsive contractor search and LAN supportANAYSSA
- 2026-09-23 14:56Add files via uploadmustaf1nx
Структура репозитория (130 файлов)
.github/workflows/pages.yml CHANGELOG.md CHECK_WINDOWS.bat QA_REPORT.md README.md START_WINDOWS.bat backend/README.md backend/data/hackathon-dataset-anonymized.jsonl backend/data_loader.py backend/filters.py backend/llm_explain.py backend/main.py backend/ranking.py backend/requirements-dev.txt backend/requirements.txt backend/synthetic_profiles.jsonl backend/tests/conftest.py backend/tests/test_backend.py docs/DESIGN_V2.md docs/desktop-dark.png docs/desktop-light.png docs/desktop-results.png docs/mobile-light.png frontend/README.md frontend/index.html frontend/package-lock.json frontend/package.json frontend/public/data/contractors.json frontend/scripts/check-core.cjs frontend/src/App.tsx frontend/src/assets/brand/icon-dark.svg frontend/src/assets/brand/icon-light.svg frontend/src/assets/brand/logo-dark.svg frontend/src/assets/brand/logo-light.svg frontend/src/assets/chevron-down.svg frontend/src/components/layout/Footer.tsx frontend/src/components/layout/Header.tsx frontend/src/components/search/CategoryBar.tsx frontend/src/components/search/ContractorCard.tsx frontend/src/components/search/DemoPresets.tsx frontend/src/components/search/Hero.tsx frontend/src/components/search/HowItWorks.tsx frontend/src/components/search/MatchIllustration.tsx frontend/src/components/search/SearchForm.tsx frontend/src/components/search/SearchResults.tsx frontend/src/components/search/SearchSummary.tsx frontend/src/components/search/StatusPanel.tsx frontend/src/components/ui/BrandLogo.tsx frontend/src/components/ui/Button.tsx frontend/src/components/ui/ErrorBoundary.tsx frontend/src/components/ui/Field.tsx frontend/src/components/ui/Icon.tsx frontend/src/components/ui/InfoDialog.tsx frontend/src/components/ui/SectionHeading.tsx frontend/src/components/ui/ThemePicker.tsx frontend/src/config/runtime.ts frontend/src/config/search.ts frontend/src/hooks/useSearch.ts frontend/src/hooks/useSearchDraft.ts frontend/src/hooks/useSectionNavigation.ts frontend/src/hooks/useTheme.ts frontend/src/main.tsx frontend/src/pages/SearchPage.tsx frontend/src/services/searchApi.test.ts frontend/src/services/searchApi.ts frontend/src/services/staticSearch.test.ts frontend/src/services/staticSearch.ts frontend/src/styles/base.css frontend/src/styles/form.css frontend/src/styles/layout.css frontend/src/styles/redesign.css frontend/src/styles/responsive.css frontend/src/styles/results.css frontend/src/styles/tokens.css frontend/src/tests/App.test.tsx frontend/src/tests/InfoDialog.test.tsx frontend/src/tests/ThemePicker.test.tsx frontend/src/tests/setup.ts frontend/src/types/search.ts frontend/src/utils/format.ts frontend/src/utils/storage.test.ts frontend/src/utils/storage.ts frontend/src/utils/theme.test.ts frontend/src/utils/theme.ts frontend/src/utils/validation.test.ts frontend/src/utils/validation.ts frontend/tsconfig.json frontend/vite.config.ts frontend/vitest.config.ts qa/backend-tests.log qa/backend-tests.xml qa/browser-checks.json qa/browser-checks.log qa/frontend-core-tests.log qa/manifest-sha256.json qa/parity_cases.json qa/transpile-result.json shabyt-design-package/design-spec.md shabyt-design-package/desktop-dark.png shabyt-design-package/desktop-dark.svg shabyt-design-package/desktop-light.png shabyt-design-package/desktop-light.svg shabyt-design-package/icon-dark.png shabyt-design-package/icon-dark.svg shabyt-design-package/icon-light.png shabyt-design-package/icon-light.svg shabyt-design-package/icon-mono-black.png shabyt-design-package/icon-mono-black.svg shabyt-design-package/icon-mono-white.png shabyt-design-package/icon-mono-white.svg shabyt-design-package/logo-cyrillic-dark.png shabyt-design-package/logo-cyrillic-dark.svg shabyt-design-package/logo-cyrillic-light.png shabyt-design-package/logo-cyrillic-light.svg shabyt-design-package/logo-cyrillic-mono-black.png shabyt-design-package/logo-cyrillic-mono-black.svg shabyt-design-package/logo-cyrillic-mono-white.png shabyt-design-package/logo-cyrillic-mono-white.svg shabyt-design-package/logo-dark.png shabyt-design-package/logo-dark.svg shabyt-design-package/logo-light.png shabyt-design-package/logo-light.svg shabyt-design-package/logo-mono-black.png shabyt-design-package/logo-mono-black.svg shabyt-design-package/logo-mono-white.png shabyt-design-package/logo-mono-white.svg shabyt-design-package/mobile-dark.png shabyt-design-package/mobile-dark.svg shabyt-design-package/mobile-light.png shabyt-design-package/mobile-light.svg
Ветки (3)
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.