AAYMA
RU
Войти

Firebird

Сервис подбора до трёх event-подрядчиков из CSV-каталога по городу, категории, дате, формату, бюджету, языку и длительности.

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

О проекте

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

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

Суть

Сервис подбора до трёх event-подрядчиков из CSV-каталога по городу, категории, дате, формату, бюджету, языку и длительности. Каждая карточка объясняется фактами, при пустой выдаче называются причины.

Как устроено

Каталог из CSV (94 профиля, 41 синтетический) загружается в память, запрос валидируется Zod. Сначала отбор по точному совпадению города и категории, затем исключение по занятости, бюджету, формату, языку и max_hours. Оставшиеся ранжируются по релевантности описания формату, потом по цене и ID, так что порядок детерминирован. При пустом результате сервис считает причины отказа и даёт контрфактические подсказки (другая дата, бюджет). Необязательный Chat Completions-адаптер может только выбрать evidence_id из готовых фрагментов, при сбое работает локальное объяснение.

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

  • rule-based filtering
  • deterministic ranking
  • counterfactual suggestions
  • rejection reason counters
  • evidence-grounded explanations
  • optional LLM evidence selection

Особенности

Различаются три состояния: подобрали; категории нет в городе; кандидаты есть, но не прошли условия. LLM ограничена выбором одного evidence_id из готовых фрагментов и не может выдумать причину. Синтетические профили явно помечены в карточках.

Стек

TypeScript · React 19 · Vite · Node.js · Zod · csv-parse · Vitest


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

README

README.md

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

Команда Obsidian · HackAlem 2026 · трек «Торговля» · задача #79-lite

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

Проект не бронирует услуги и не отправляет заявки. Его задача — сделать первый выбор подрядчика объяснимым и быстрым.

Почему это ценно

В event-каталоге можно найти десятки похожих профилей, но заказчику трудно понять, кого выбрать и почему. Сервис превращает условия заказа в прозрачный короткий список:

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

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

Что реализовано

  • Web-интерфейс с формой заказа и шестью готовыми демо-сценариями.
  • Подбор не более трёх подрядчиков из каталога по городу, категории, дате, формату, бюджету, языку и длительности.
  • Проверка календаря занятости для всех категорий, включая банкетные залы.
  • Три явных состояния: подобрали, категории нет в городе, кандидаты есть, но ни один не прошёл условия.
  • Счётчики причин отказа и безопасные контрфактические подсказки: другая дата, достаточный бюджет, снятие требования к языку или длительности.
  • В успешной выдаче — прозрачная строка скрининга: сколько профилей отсеяно и по каким ненулевым причинам.
  • Ввод бюджета с точностью до 1 ₸; кандидат со стартовой ценой, равной бюджету, остаётся в выдаче.
  • В интерфейсе показан короткий маршрут демо, а у пустого результата дополнительно названа главная причина отказа.
  • Детерминированный порядок: один и тот же запрос возвращает одинаковые карточки даже при изменении порядка строк CSV.
  • Карточка с ценой «от», городом, категорией, признаками исходных данных и индивидуальным объяснением.
  • HTTP API, валидация запросов, production-сборка и автоматические тесты.

Как работает подбор

React-интерфейс
      │ POST /api/recommend
      ▼
Node.js HTTP API ── Zod-валидация запроса
      │
      ▼
CSV-каталог в памяти
      │  1. город + категория
      │  2. исключение: занятость / бюджет / формат / язык / длительность
      │  3. ранжирование: релевантность описания → цена → ID
      ▼
До 3 карточек + доказательное объяснение + причины пустого результата

При старте сервер читает data/catalog.csv в память; база данных и учётная запись не требуются. Вход проверяется через Zod, поэтому API принимает только корректную дату в окне календаря, положительный бюджет и значения из каталога.

Правила отбора и порядка

  1. Берутся только профили с точным совпадением города и категории.
  2. Профиль исключается, если он занят в дату, его стартовая цена выше бюджета, он не берёт формат, не работает на запрошенном языке или его max_hours меньше нужной длительности. Цена, равная бюджету, допустима. Для профилей без ограничения присутствия max_hours = null длительность не ограничивает выдачу.
  3. У оставшихся профилей ищутся конкретные предложения в description, связанные с форматом события. Совпадения терминов формата дают приоритет; далее применяются стартовая цена и id. Поэтому результат стабилен и не зависит от порядка CSV.
  4. Показываются первые три кандидата. Если их меньше трёх, интерфейс прямо указывает фактическое число.

Объяснимость и AI

По умолчанию сервис работает целиком локально. Каждое объяснение содержит факты, уже прошедшие фильтры: отсутствие отметки о занятости на дату, формат, цену относительно бюджета, а при наличии — язык и длительность. В конце выводится буквальный фрагмент описания профиля.

В коде есть необязательный адаптер совместимого с Chat Completions AI API. Он не создаёт рекомендации, не меняет порядок и не придумывает факты: из заранее подготовленных фрагментов он может выбрать только один наиболее уместный evidence_id для каждой уже выбранной карточки. Ответ проходит строгую проверку; при тайм-ауте, ошибке или некорректном ответе сервис автоматически использует локальное объяснение. Основной сценарий и все проверки работают без AI-ключа.

Такой подход сочетает смысловую помощь AI с контролируемой, воспроизводимой логикой: модель не становится непрозрачным ранжировщиком и не может выдать неподтверждённую причину.

Данные и внешние компоненты

  • Каталог находится в hack-ca9ed74e-obsidian/firebird/data/catalog.csv: в актуальной версии 94 профиля, из них 41 помечен как synthetic. Исходный набор организатора был расширен синтетическими профилями для демонстрационного покрытия редких категорий, пустых сценариев и разных городов. Флаги synthetic, city_imputed и price_imputed сохраняются при загрузке; в карточках заметен признак synthetic.
  • Проект не добавляет скрытые данные, не обучает модель на каталоге и не передаёт каталог внешнему сервису в локальном режиме.
  • Открытые npm-зависимости: TypeScript, React, Vite, Node.js, Zod, csv-parse, dotenv, Vitest и lucide-react. Их версии и команды запуска описаны в package.json.
  • Внешний AI API — опциональный компонент, включаемый только переменными окружения ниже. Для технической проверки он не нужен; ключи и персональные аккаунты не требуются.

Технологии

СлойРешение
КлиентReact 19, TypeScript, Vite, lucide-react
СерверNode.js HTTP server, TypeScript, tsx
Валидация и данныеZod, csv-parse, CSV в памяти
ПроверкиVitest, TypeScript compiler, Vite production build
Опциональная надстройкаAPI, совместимый с Chat Completions, с JSON-контрактом и fallback

Установка и запуск с нуля

Основной сценарий работает полностью локально. Не нужны база данных, личный аккаунт, платная подписка или AI-ключ.

1. Установите Node.js один раз

Скачайте и установите актуальную LTS-версию Node.js с nodejs.org. Нужна версия 22.13.0 или новее. Установщик Node.js включает npm. После установки закройте и заново откройте PowerShell, затем проверьте командами:

node --version
npm --version

Должна появиться версия Node.js наподобие v22.13.0 или новее и номер версии npm. Если одна из команд не найдена, Node.js ещё не установлен или PowerShell нужно перезапустить.

2. Получите проект и откройте PowerShell

Если репозиторий ещё не скачан, откройте PowerShell в папке, где хотите хранить проект, и выполните:

git clone https://github.com/BAITC-Hacks/hack-ca9ed74e-obsidian.git

Если проект уже есть на компьютере, откройте PowerShell в папке, где лежит внешняя папка hack-ca9ed74e-obsidian. Затем перейдите в папку приложения:

cd .\hack-ca9ed74e-obsidian\firebird

Проверьте, что всё верно:

Get-Location

Путь должен оканчиваться на hack-ca9ed74e-obsidian\firebird.

3. Установите зависимости

npm install

Команда скачивает библиотеки, перечисленные в package.json. Это нужно сделать только один раз для каждого нового компьютера или после удаления папки node_modules. Права администратора и отдельный менеджер пакетов не требуются.

Если установка завершилась строкой Done, переходите дальше. Интернет нужен только на этом этапе.

4. Проверьте проект перед запуском

node .\scripts\run.mjs check
node .\scripts\run.mjs smoke

Первая команда проверяет TypeScript и запускает 31 автоматический тест. Вторая прогоняет шесть демонстрационных сценариев: плотную и редкую категории, смену даты, отсутствие категории, жёсткие условия и декабрьскую занятость. Эти команды не используют npm или pnpm; нужен только уже установленный Node.js и папка node_modules.

5. Соберите и запустите приложение

node .\scripts\run.mjs build
node .\scripts\run.mjs start

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

http://127.0.0.1:3001

Не закрывайте это окно PowerShell, пока проверяете сайт. Откройте в браузере http://127.0.0.1:3001, выберите один из демо-запросов в интерфейсе или заполните форму и нажмите «Подобрать».

Чтобы остановить сервер, вернитесь в это окно PowerShell и нажмите Ctrl+C.

6. Режим разработки — только если меняете код

Вместо production-запуска можно выполнить:

node .\scripts\run.mjs dev

Откройте http://127.0.0.1:5173. В этом режиме изменения в интерфейсе подхватываются автоматически; сервер API работает на http://127.0.0.1:3001.

Если что-то пошло не так

Что видноЧто сделать
node не является командойУстановите Node.js LTS, закройте и заново откройте PowerShell.
Тест пишет, что не найден модульВыполните npm install один раз в папке firebird, затем повторите node .\scripts\run.mjs check.
Браузер не открывает сайтУбедитесь, что node .\scripts\run.mjs start всё ещё работает и в терминале есть адрес 127.0.0.1:3001.
Порт 3001 занятВ новом PowerShell, в папке firebird, выполните $env:PORT=3002, затем node .\scripts\run.mjs start и откройте http://127.0.0.1:3002.

Переменные окружения

Шаблон — hack-ca9ed74e-obsidian/firebird/.env.example. Файл .env необязателен: без него сервис использует 127.0.0.1:3001 и локальные объяснения.

ПеременнаяПо умолчаниюНазначение
PORT3001Порт HTTP-сервера
HOST127.0.0.1Хост HTTP-сервера
AI_ENDPOINT—HTTPS URL совместимого Chat Completions API
AI_MODEL—Имя модели провайдера
AI_API_KEY—Ключ провайдера; не коммитить
AI_TIMEOUT_MS4500Тайм-аут AI от 500 до 7000 мс

AI включается, только если одновременно заданы AI_ENDPOINT, AI_MODEL и AI_API_KEY. При его отсутствии или недоступности подбор остаётся работоспособным.

Основной сценарий проверки

Проверяющий может выполнить всё через интерфейс, не создавая учётную запись.

СценарийПараметрыЧто должно быть видно
Плотная категорияАлматы · Ведущий · корпоратив · 10.10.2026 · 1 000 000 ₸ · 6 ч · русскийТри карточки из четырёх подходящих; у каждой своё объяснение и источник из описания. Над карточками видны число отсеянных профилей и причины.
Та же заявка, другая датаНажмите «Другая дата» — 17.10.2026Состав карточек меняется только из-за календаря; в объяснениях новая дата.
Редкая категорияАлматы · Флорист · свадьба · 10.10.2026 · 300 000 ₸Показано меньше трёх карточек и честное число найденных кандидатов.
Категории нетЗарубежье · Фото и видеобудкиОтдельное сообщение: в городе нет такой категории.
Кандидаты есть, но условия не подходятАлматы · Ведущий · корпоратив · 10.10.2026 · 100 000 ₸ · 6 ч · русскийПустой результат с числом отказов по причинам и полезной подсказкой по бюджету.
Пиковая занятостьПресет «Декабрьский пик»Видно, что пустая выдача связана с занятостью, а не с ошибкой интерфейса.

Повторите любой из запросов: порядок карточек не изменится. Измените только дату в первом сценарии — выдача изменится согласно busy_dates, а не случайно.

API для независимой проверки

Метод и маршрутРезультат
GET /api/healthСтатус сервера и число загруженных профилей
GET /api/catalogДопустимые значения фильтров, границы календаря, метаданные каталога
POST /api/recommendПодбор, причины исключения, подсказки и карточки

Пример тела запроса:

{
  "city": "Алматы",
  "date": "2026-10-10",
  "format": "корпоратив",
  "category": "Ведущий",
  "budget": 1000000,
  "hours": 6,
  "language": "русский"
}

Успешный ответ содержит status: "matched", eligibleCount, не более трёх recommendations, reasons, suggestions, режим объяснения и elapsedMs. Возможные status: matched, category_absent, no_match.

Проверки качества

Набор Vitest-проверок покрывает:

  • загрузку всех записей каталога и сохранение признаков происхождения;
  • календарь на всём 100-дневном окне, включая декабрьскую плотность;
  • точные границы бюджета и длительности, язык, формат и занятость;
  • отсутствие дубликатов в категориях с несколькими услугами;
  • три пользовательских состояния и контрфактические подсказки;
  • детерминизм при повторном запуске и перестановке строк каталога;
  • API, ошибки валидации, работу без AI и безопасный fallback AI-адаптера.

На актуальной версии проекта node .\scripts\run.mjs check проходит: 3 test files, 31 tests passed; node .\scripts\run.mjs smoke проходит шесть демо-сценариев, а node .\scripts\run.mjs build успешно собирает production-клиент.

Потенциал развития

Сейчас это прозрачный слой выбора поверх статического каталога. Для площадки-агрегатора он может развиться без замены ядра:

  • подключать реальные календари и обновления цен из кабинетов подрядчиков;
  • учитывать дополнительные фильтры: вместимость, район, тип площадки, рейтинг и доступность команды;
  • хранить обратную связь о том, какие объяснения и карточки действительно помогли выбрать;
  • масштабироваться на новые города, категории и другие вертикали услуг, сохраняя доказательные причины выбора;
  • добавлять LLM-помощника только в ограниченной роли интерпретации проверяемых данных, оставляя фильтры и порядок аудируемыми.

Структура репозитория

hack-ca9ed74e-obsidian/           # внешняя папка репозитория
├── README.md
└── firebird/                     # приложение
    ├── data/catalog.csv          # каталог задачи
    ├── src/client/               # React-интерфейс
    ├── src/domain/               # отбор, ранжирование, объяснения
    ├── src/server/               # API, загрузка каталога, AI-адаптер
    ├── tests/                    # unit- и API-тесты
    ├── docs/PLAN.md              # план этапов хакатона
    ├── .env.example              # безопасный шаблон окружения
    └── package.json              # команды и зависимости

История разработки и промежуточные результаты сохраняются в официальном GitHub-репозитории команды. Перед финальной проверкой запускайте node .\scripts\run.mjs check, node .\scripts\run.mjs smoke, node .\scripts\run.mjs build и основной демо-сценарий выше.

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

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

Kirpi4k 22bikineevaviktoria7-create 7DaLeXoVsKiY 1

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

  • 2026-09-23 17:43Fix README inconsistenciesKirpi4k
  • 2026-09-23 17:41Shorten empty result status copyKirpi4k
  • 2026-09-23 17:26Remove synthetic count from headerKirpi4k
  • 2026-09-23 17:24Polish contractor card source detailsKirpi4k
  • 2026-09-23 17:16Expand synthetic contractor coverageKirpi4k
  • 2026-09-23 17:00Polish recommendation page layoutKirpi4k
  • 2026-09-23 16:53Merge branch 'main' of https://github.com/BAITC-Hacks/hack-ca9ed74e-obsidian # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into bikineevaviktoria7-create
  • 2026-09-23 16:48docs: simplify startup without pnpmbikineevaviktoria7-create
Все коммиты (30)
  • 2026-09-23 16:47MVP: polish numeric input unitsKirpi4k
  • 2026-09-23 16:46We fixed the login errors.bikineevaviktoria7-create
  • 2026-09-23 16:33MVP: polish empty state layoutKirpi4k
  • 2026-09-23 16:32MVP: polish empty state layoutKirpi4k
  • 2026-09-23 16:29MVP: polish form and header UIKirpi4k
  • 2026-09-23 16:21MVP: hide response timing badgeKirpi4k
  • 2026-09-23 16:19docs: clarify installation and startupbikineevaviktoria7-create
  • 2026-09-23 16:14MVP: add demo route hintsKirpi4k
  • 2026-09-23 16:08MVP: add demo smoke checksKirpi4k
  • 2026-09-23 16:02docs: clarify installation and startupbikineevaviktoria7-create
  • 2026-09-23 15:58MVP: polish app title and explain matching pipelineKirpi4k
  • 2026-09-23 15:45Merge branch 'main' of https://github.com/BAITC-Hacks/hack-ca9ed74e-obsidianKirpi4k
  • 2026-09-23 15:42Firebird: allow exact budget valuesKirpi4k
  • 2026-09-23 15:35docs: update project READMEbikineevaviktoria7-create
  • 2026-09-23 15:28Firebird: keep ignore rules at repository rootKirpi4k
  • 2026-09-23 15:22Firebird: show screening reasons in successful resultsKirpi4k
  • 2026-09-23 15:03Update READMEbikineevaviktoria7-create
  • 2026-09-23 14:50README.md modifiedDaLeXoVsKiY
  • 2026-09-23 14:36Firebird: add working demo interface and delete READMEKirpi4k
  • 2026-09-23 14:17Merge organizer repositoryKirpi4k
  • 2026-09-23 14:14Firebird: базовый подбор подрядчиковKirpi4k
  • 2026-09-23 13:58Первый комит (Структура)Kirpi4k
Структура репозитория (28 файлов)
.gitignore
README.md
hack-ca9ed74e-obsidian/firebird/.env.example
hack-ca9ed74e-obsidian/firebird/data/catalog.csv
hack-ca9ed74e-obsidian/firebird/docs/PLAN.md
hack-ca9ed74e-obsidian/firebird/index.html
hack-ca9ed74e-obsidian/firebird/package.json
hack-ca9ed74e-obsidian/firebird/pnpm-lock.yaml
hack-ca9ed74e-obsidian/firebird/pnpm-workspace.yaml
hack-ca9ed74e-obsidian/firebird/public/favicon.svg
hack-ca9ed74e-obsidian/firebird/scripts/run.mjs
hack-ca9ed74e-obsidian/firebird/scripts/smoke.ts
hack-ca9ed74e-obsidian/firebird/src/client/App.tsx
hack-ca9ed74e-obsidian/firebird/src/client/main.tsx
hack-ca9ed74e-obsidian/firebird/src/client/styles.css
hack-ca9ed74e-obsidian/firebird/src/domain/explanations.ts
hack-ca9ed74e-obsidian/firebird/src/domain/match.ts
hack-ca9ed74e-obsidian/firebird/src/server/ai.ts
hack-ca9ed74e-obsidian/firebird/src/server/app.ts
hack-ca9ed74e-obsidian/firebird/src/server/catalog.ts
hack-ca9ed74e-obsidian/firebird/src/server/index.ts
hack-ca9ed74e-obsidian/firebird/src/shared/schema.ts
hack-ca9ed74e-obsidian/firebird/src/shared/types.ts
hack-ca9ed74e-obsidian/firebird/tests/ai.test.ts
hack-ca9ed74e-obsidian/firebird/tests/api.test.ts
hack-ca9ed74e-obsidian/firebird/tests/match.test.ts
hack-ca9ed74e-obsidian/firebird/tsconfig.json
hack-ca9ed74e-obsidian/firebird/vite.config.ts

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

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

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