AAYMA
RU
Войти

Таңда

Сервис подбирает до трёх свободных event-подрядчиков под параметры заказа и объясняет выбор и порядок фактами из профилей.

Доведённый продукт
Команда «pITchers» 21💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: pITchers
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Доведённый продукт

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

Суть

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

Как устроено

66 профилей из CSV нормализуются в памяти. Жёсткие фильтры идут в фиксированном порядке (занятость → бюджет → формат → язык → часы), и каждый отсеянный получает одну причину. Детерминированный скоринг складывает 5 компонентов: совпадение с описанием 35%, запас бюджета 25%, специализация 15%, часы 15%, качество данных 10%. Итог — четыре статуса (found/partial/all_filtered/no_category) с подсказками. LLM (temperature 0) пишет объяснения только по фактам движка, ответ проверяется по числам, цитатам и различимости; при провале одна перегенерация, затем локальный шаблон. Свободный текст запроса разбирается через function calling со строгими enum.

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

  • детерминированный взвешенный скоринг
  • жёсткие фильтры с причинами отсева
  • function calling
  • валидация ответа LLM по фактам
  • rankRationale
  • кэш проверенных объяснений
  • selfcheck API

Метрики

72 теста пройдено; selfcheck 10/10; метрики ядра на 918 запросах; детерминизм OK

Особенности

Честная пустая выдача: когда подходящих нет, сервис называет число кандидатов, причины отсева и предлагает альтернативы кнопками. Отдельно объясняется порядок карточек (rankRationale с разницей баллов). Проверка каждого ответа LLM по фактам с резервным шаблоном, так что запрос никогда не падает из-за модели. Ниша задаётся конфигом в domain.json.

Стек

Node.js · Express · JavaScript · Playwright · HTML/CSS

Модели

OpenAI SDK


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

README

README.md

Таңда

Подбор event-подрядчиков с объяснением. Команда pITchers, HackAlem AI 2026, трек «Креативные индустрии», кейс #79-lite.

«Таңда» по-казахски — «выбирай». Сервис не просто сортирует каталог: он показывает до трёх подрядчиков и словами объясняет, почему именно они и почему в таком порядке.

tests selfcheck Node.js Express OpenAI SDK

Проблема

Заказчик открывает каталог площадки и видит длинный список карточек, одинаковых по виду: цена «от», список форматов, несколько строк о себе — и никакого ответа на вопрос, кого из них брать и чем один лучше другого. В сезон список сжимается ещё сильнее: по данным кейса в декабре подрядчик занят в среднем 75% дней, а на 26 декабря заняты 52 профиля из 66. Когда после всех фильтров экран оказывается пустым и не объясняет почему, заказчик уходит искать в мессенджерах — для площадки это потерянный клиент, хотя подходящий подрядчик обычно есть на соседней дате или чуть дороже.

Решение

Сервис принимает параметры заказа и возвращает до трёх свободных подрядчиков. У каждого — объяснение из проверяемых фактов его профиля: цена, лимит часов, языки, цитата из описания. Отдельно возвращается rankRationale — почему первая карточка стоит выше второй: «№1 впереди №2 на 0.0167: запас бюджета — 900 000 ₸ против 1 000 000 ₸». Если подходящих меньше трёх или нет совсем, ответ всё равно содержательный: сколько кандидатов было, кто и почему отсеян, и готовые альтернативы одним нажатием — другая дата («Показать на 16.12.2026»), больший бюджет («Показать с бюджетом от 650 000 ₸») или другой город («Показать в Алматы (3)»).

Для кого и как зарабатывает

Продукт B2B: агрегаторы и маркетплейсы event-услуг, у которых уже есть каталог подрядчиков с ценами и календарями. Подключение — через API (POST /api/recommend) или встраиваемый виджет на существующую страницу поиска; своей базы сервис не ведёт и данные площадки не подменяет.

Модели монетизации: подписка на API по объёму запросов либо оплата за заявку, доведённую до подрядчика. Пилот устроен так, чтобы его можно было запустить за один спринт: площадка отдаёт один город и одну категорию в формате нашего CSV, мы подключаем POST /api/recommend и сравниваем поведение пользователей на выдаче с объяснениями и без неё.

Почему мы

  • Объяснения только из проверяемых фактов. Модель получает факты карточек, а не свободу выдумывать: ответ валидируется по числам и цитатам из профиля, запрещённые общие фразы отсекаются. Не прошёл проверку — текст собирается локально из тех же фактов, запрос не падает.
  • Детерминизм. Тот же запрос даёт тот же порядок и те же баллы; результат воспроизводится командой и проверяется автотестами. Площадка может повторить нашу выдачу у себя и убедиться в ней.
  • Честная пустая выдача. Вместо «ничего не найдено» — сколько было кандидатов, у кого какая причина отказа и какая ближайшая дата или бюджет всё меняют.
  • Новая ниша — это конфиг, а не разработка. Ключевые слова, веса формулы и порядок фильтров вынесены в config/domain.json; ядро не знает, что речь о мероприятиях.

Метрики пилота

Что измеряем на площадке-партнёре, чтобы решение было видно в цифрах, а не на словах:

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

Базовые значения снимаются на текущей выдаче площадки до подключения — без этого сравнение бессмысленно.

Дорожная карта

  1. Заявка и бронирование прямо из карточки вместо перехода в мессенджер.
  2. Синхронизация календарей подрядчиков, чтобы занятость обновлялась сама, а не выгрузкой.
  3. Объяснения на казахском языке наравне с русским.
  4. Отзывы и подтверждённые заказы как ещё один источник фактов для объяснений.
  5. Соседние ниши на том же ядре: репетиторы, клиники, строительные бригады.

Как выглядит

Подобрали трёхОсталась однаКатегории нет в городе
Три карточкиОдна карточкаНет категории

Как запустить

Работает без ключа LLM. npm ci && npm run preview поднимает сайт с реальными данными и объяснениями, собранными из фактов каталога. Ключ нужен только для LLM-режима.

npm ci
npm run preview     # сайт без внешних вызовов
npm test            # 72 теста, без сети
npm run demo        # 5 сценариев + проверка детерминизма

Демо-запросы и ожидаемый результат

Все пять воспроизводятся командой npm run demo; первые три есть на сайте кнопками «Демо-запросы».

ЗапросОжидаемый статусЧто показывает сервис
Ведущий, Алматы, корпоратив, 17.10.2026, 1 500 000 ₸found3 карточки: «Из 10 подрядчиков категории «Ведущий» в Алматы подходят ровно 3 — показываем всех.»
Тот же запрос на 26.12.2026partial1 карточка: «…подходит только 1: 9 заняты 26.12.2026.»
Флорист, Алматы, свадьба, 17.10.2026, 500 000 ₸partial1 из 2, второй занят; профиль помечен как синтетический
Банкетный зал, Алматы, корпоратив, 19.12.2026, 3 000 000 ₸all_filtered0 карточек: «…не подходит ни один: 7 заняты 19.12.2026. Ближайшая подходящая дата — 16.12.2026.»
Декоратор, Астана, свадьба, 17.10.2026, 1 000 000 ₸no_category0 карточек: «В Астане нет подрядчиков категории «Декоратор». В Алматы — 3.» + кнопка «Показать в Алматы (3)»

Повторный запуск любого запроса даёт тот же порядок карточек и те же баллы — npm run demo прогоняет каждый сценарий дважды и печатает «Детерминизм: OK».

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

  • Данные. 66 профилей из CSV организаторов загружаются в память и нормализуются: списки категорий, форматов и языков, календарь занятости, флаги synthetic, cityImputed, priceImputed. Исходный CSV не меняется.
  • Жёсткие фильтры с причинами. Порядок фиксирован: занятость → бюджет → формат → язык → часы. Каждый отсеянный получает ровно одну причину — первую непройденную — и конкретную расшифровку: «занят 26.12.2026», «от 2 000 000 ₸ при бюджете 1 500 000 ₸».
  • Детерминированное ранжирование. Пять компонентов в 0..1: совпадение с описанием 35%, запас бюджета 25%, специализация 15%, часы 15%, качество данных 10%. Округление до 4 знаков, при равенстве баллов — сортировка по id.
  • Четыре исхода. found (3 карточки), partial (1–2), all_filtered (кандидаты есть, но никто не проходит), no_category (в этом городе категории нет). У каждого — русское сообщение с конкретными числами.
  • Подсказки и действия. nearestFreeDate (ближайшая дата в ±14 дней), minBudgetNeeded, список других городов — и готовые кнопки: «Показать на 16.12.2026», «Показать с бюджетом от 650 000 ₸», «Показать в Алматы (3)».
  • Объяснения. Один запрос к LLM на все карточки, temperature 0, только факты движка — имена и баллы модели не передаются. Ответ валидируется: состав ID, длина, числа, попарная различимость. Невалидный ответ → одна перегенерация → локальный резерв из фактов. Запрос никогда не падает из-за модели.
  • Объяснение порядка. rankRationale говорит, чем первая карточка обошла вторую: «№1 впереди №2 на 0.0167: запас бюджета — 900 000 ₸ против 1 000 000 ₸ (0.4 против 0.3333). Совпали: часы, специализация.» Объясняется не только выбор, но и ранжирование; имена в текст не попадают, поэтому он работает и в режиме «без имён».
  • Гарантия различимости. У каждой показанной карточки есть минимум одно измеримое отличие от остальных: ранг цены, уникальный лимит часов, язык, ширина профиля, лучший компонент рейтинга или слово из описания, которого нет у других.
  • Агентный ввод. POST /api/parse разбирает фразу «Нужен ведущий на корпоратив в Алматы 14 ноября, бюджет 800 тысяч» через function calling со строгими enum из каталога. Не хватает поля — возвращается один уточняющий вопрос. Нет ключа — форма продолжает работать.
  • Сравнение дат. POST /api/compare показывает, кто выпал из выдачи при смене даты и почему.
  • Живая самопроверка. GET /api/selfcheck прогоняет 10 проверок требований кейса на реальных данных и возвращает отчёт.

Пример проверки

npm test                  # 72 теста: ядро, AI, HTTP, контракты. Без внешних вызовов
npm run demo              # 5 сценариев, статусы и «Детерминизм: OK»
npm start                 # сервер на http://localhost:3000
curl http://localhost:3000/api/selfcheck

Свежие цифры лежат в репозитории и читаются без запуска: docs/selfcheck.md — отчёт живой самопроверки с датой прогона, docs/metrics.md — метрики ядра на 918 запросах по всему каталогу. Оба файла пересобираются командой node scripts/report.js.

GET /api/selfcheck на текущей версии возвращает 10 из 10:

ПроверкаРезультат
Детерминизм демо №1HK-35215, HK-44733, HK-77838 — оба прогона совпали
Все показанные подрядчики свободныпроверено 12 карточек по календарям, занятых 0
Смена даты на 26.12 меняет выдачувыпали 3, у каждого причина «занят 26.12.2026»
Плотная категорияfound, 3 карточки
Редкая категория объясненаpartial, 1 карточка, сказано почему
Нет категории в городеno_category + другие города
Полный отсев с подсказкамиall_filtered + ближайшая дата
Каждый запрос быстрее 10 000 мссамый медленный сценарий укладывается в предел
Объяснения попарно различимыу всех трёх карточек своё число или цитата
Нет запрещённых общих фразпроверено 8 объяснений, нарушений 0

API

Полные примеры запросов и ответов: docs/api.md. Схема пайплайна: docs/pipeline.md.

МетодНазначение
GET /api/metaгорода, категории, форматы, языки, границы календаря, размер каталога
POST /api/recommendподбор: тело — Query, ответ — EngineResult с объяснениями и elapsedMs
POST /api/compareсравнение двух дат: { query, date2 }
POST /api/parseразбор свободного текста в Query
GET /api/selfcheckотчёт из 10 проверок требований
curl -X POST http://localhost:3000/api/recommend \
  -H "Content-Type: application/json" \
  -d '{"city":"Алматы","date":"2026-10-17","eventType":"корпоратив","category":"Ведущий","budget":1500000}'
{
  "status": "found",
  "message": "Из 10 подрядчиков категории «Ведущий» в Алматы подходят ровно 3 — показываем всех.",
  "candidatesTotal": 10,
  "excluded": { "busy": 5, "over_budget": 1, "format": 1, "language": 0, "hours": 0 },
  "commonFacts": ["все свободны 17.10.2026", "все работают с форматом «корпоратив»", "…"],
  "cards": [
    {
      "id": "HK-35215", "name": "Кики", "priceFrom": 900000, "score": 0.495,
      "scoreParts": { "relevance": 0.2, "budget": 0.4, "specialization": 1, "hours": 0.5, "dataQuality": 1 },
      "factChips": ["900 000 ₸ — 60% бюджета", "самый доступный из показанных", "больше всех часов — до 10 ч"],
      "explanation": "Самый большой лимит присутствия среди показанных: 10 ч, в описании: «тимбилдинги, конференции и крупные события». Цена от 900 000 ₸.",
      "explanationSource": "llm"
    }
  ],
  "elapsedMs": 2723
}

Ошибка валидации возвращает 400 с русским текстом:

{ "error": "Дата 05.01.2027 вне календаря подрядчиков: 23.09.2026 — 31.12.2026." }

Технологии и архитектура

Node.js 24, ES modules, Express 5, csv-parse, dotenv, SDK openai. Фронтенд — HTML, CSS и vanilla JS без сборки, иконки Lucide отдаются локально. Тесты: встроенный node:test и Playwright. Базы данных, сборки фронтенда и дообучения модели нет.

CSV → нормализация → кандидаты (город + категория) → жёсткие фильтры с причинами
    → баллы 0..1 → топ-3 + факты и отличия → статус, подсказки, действия
    → объяснения (кэш / LLM с таймаутом / резерв из фактов) → EngineResult
ПутьНазначениеАвтор
data/contractors.csvисходные данные организаторов, не изменяются—
config/domain.jsonключевые слова, веса формулы, порядок фильтровДамир
src/engine/загрузка данных, фильтры, баллы, статусы, факты, сравнение датДамир
src/agent/разбор свободного текста через function callingДамир
src/api//api/compare, /api/selfcheckДамир, участник 3
src/ai/промпт, валидация ответа, кэш, резервные объясненияучастник 2
src/server.jsExpress-приложение, таймауты, обработка ошибокучастник 3
public/интерфейс: форма, карточки, причины отсева, избранноеучастник 3
scripts/, tests/, docs/демо, автотесты, документацияДамир

Ядро не знает предметной области: ключевые слова, веса и порядок фильтров вынесены в config/domain.json. Как перенести сервис на другую нишу — в docs/pipeline.md.

Установка

node --version        # нужен Node.js 24
npm ci
node src/engine/data.js   # 66 профилей, 17 категорий — проверка данных
npm run preview           # сайт без ключа

Для LLM-режима создайте личный .env в корне (шаблон — .env.example):

LLM_API_KEY=your-own-key
LLM_BASE_URL=
LLM_MODEL=gpt-4.1-mini
PORT=3000

LLM_BASE_URL пуст для OpenAI; для NVIDIA — https://integrate.api.nvidia.com/v1. Нужна модель с поддержкой Chat Completions, JSON-ответов и temperature=0. Поддержаны резервные имена OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL. .env, node_modules и .cache не коммитятся. После настройки ключа: node src/ai/warm-cache.js --refresh, затем npm start.

explanationSource в ответе показывает происхождение текста: llm — принятый ответ модели, cache — сохранённый ответ модели, template — локальный резерв из фактов. Шаблоны не кэшируются.

Данные и внешние сервисы

Датасет организаторов: 66 анонимизированных профилей, 17 категорий, 3 города, календари с 23.09.2026 по 31.12.2026; 13 записей полностью синтетические, у части профилей город или цена восстановлены — всё это помечено флагами и видно на карточке. Движок поддерживает необязательный data/synthetic.csv с теми же колонками.

Внешний сервис один — провайдер LLM по вашему ключу. В него уходят только запрос пользователя и факты выбранных карточек: имена, баллы и сырой CSV не передаются. Без ключа сервис работает полностью локально.

Ограничения

  • Свободная дата означает отсутствие занятости в предоставленном календаре, а не подтверждённую бронь. Цена «от» не гарантирует итоговую смету.
  • Описания — заявления самих профилей; независимой проверки опыта и отзывов нет.
  • Если у карточек совпадают все факты, сервис не придумывает различий, а говорит об этом прямо.
  • Повторяемость LLM-текста после перезапуска опирается на файловый кэш. Если кэш удалён, текст может отличаться даже при temperature=0; порядок и баллы карточек при этом не меняются.
  • Кэш рассчитан на один процесс Node.js, многопроцессной синхронизации нет.
  • Предел 10 секунд относится к пользовательскому сценарию: внутри стоят таймауты 8 с на AI и 9 с на сервер, но сеть и загрузка машины тоже влияют.
  • Избранное хранится только в localStorage браузера. Бронирования, контактов, уведомлений и аккаунтов нет.
  • Публичного развёртывания нет: проект запускается локально, ссылки на деплой не существует.
  • Фото в боковой панели — иллюстрация сезона, не изображает конкретного подрядчика; источник указан в public/assets/README.md.
Звёзды
⭐ 0
Форки
0
Язык
JavaScript
Последний push
в прошлом месяце
Коммиты
41
Авторы
3
Файлы
55
Ветки
1

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

Damir 25Tanirberdi Bolat 11Bekzat 5

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

  • 2026-09-23 17:55feat(ui): redesign interface with animationsBekzat
  • 2026-09-23 17:48docs: add README screenshotsDamir
  • 2026-09-23 17:45fix(ai): accept valid model answers and voice new differentiator tagsDamir
  • 2026-09-23 17:39docs: add product overview to READMEDamir
  • 2026-09-23 17:21feat(engine): explain the ranking order with rankRationaleDamir
  • 2026-09-23 17:19docs: generate selfcheck and engine metrics reportsDamir
  • 2026-09-23 17:12test(engine): guard the EngineResult contract across all statusesDamir
  • 2026-09-23 17:11docs: rewrite README for judges and document the APIDamir
Все коммиты (41)
  • 2026-09-23 16:56feat(web): deliver responsive vendor matching workspaceTanirberdi Bolat
  • 2026-09-23 16:35feat(api): add live requirements self-checkDamir
  • 2026-09-23 16:20feat(ui): build contractor recommendation interfaceBekzat
  • 2026-09-23 16:08feat(engine): guarantee differentiators and add common factsDamir
  • 2026-09-23 16:08feat(engine): add two-date comparisonDamir
  • 2026-09-23 15:57feat(ai): verify live demo cache warmup and document handoffTanirberdi Bolat
  • 2026-09-23 15:55feat(server): integrate engine logicBekzat
  • 2026-09-23 15:53feat(ai): add anonymous demo comparison scriptTanirberdi Bolat
  • 2026-09-23 15:51fix(ai): lead fallback explanations with individual evidenceTanirberdi Bolat
  • 2026-09-23 15:48fix(ai): validate pairwise evidence and retry onceTanirberdi Bolat
  • 2026-09-23 15:46feat(ai): prompt with common facts and comparative evidenceTanirberdi Bolat
  • 2026-09-23 15:43fix(ai): allow an eight-second explanation deadlineTanirberdi Bolat
  • 2026-09-23 15:42fix(ai): cache only verified LLM explanationsTanirberdi Bolat
  • 2026-09-23 15:33fix(ai): preserve timeout determinism and verify HTTP demoTanirberdi Bolat
  • 2026-09-23 15:22docs: align README with team API integrationTanirberdi Bolat
  • 2026-09-23 15:21feat(ai): add grounded explanations, cache and jury documentationTanirberdi Bolat
  • 2026-09-23 15:14refactor(engine): move domain settings to configDamir
  • 2026-09-23 15:05test(engine): add requirement checksDamir
  • 2026-09-23 14:56feat(server): add recommendation API with explanation fallbackDamir
  • 2026-09-23 14:47chore: ignore local notesDamir
  • 2026-09-23 14:40docs: add pipeline overview and env exampleDamir
  • 2026-09-23 14:30feat(agent): add text request parser with function callingDamir
  • 2026-09-23 14:19feat(engine): add excludedList, actions and fact chipsDamir
  • 2026-09-23 14:10feat(engine): add demo script with determinism checkDamir
  • 2026-09-23 14:08feat(engine): add recommend with statuses, hints and factsDamir
  • 2026-09-23 14:04feat(engine): add deterministic scoring with score partsDamir
  • 2026-09-23 14:02feat(engine): add hard filters with exclusion reasonsDamir
  • 2026-09-23 14:00ТЕКСТDamir
  • 2026-09-23 13:56chore: switch to ES modules, add start and demo scriptsDamir
  • 2026-09-23 13:50chore: init node projectDamir
  • 2026-09-23 13:41fix: add .gitignore and remove node_modules from gitBekzat
  • 2026-09-23 13:39feat(frontend): setup initial project structure and packagesBekzat
  • 2026-09-23 13:32chore: add agents rules, dataset and gitignoreDamir
Структура репозитория (55 файлов)
.env.example
.gitignore
AGENTS.md
CLAUDE.md
README.md
config/domain.json
data/contractors.csv
docs/api.md
docs/metrics.md
docs/pipeline.md
docs/selfcheck.md
package-lock.json
package.json
playwright.config.js
public/app.js
public/assets/README.md
public/assets/event-table.jpg
public/assets/screens/01-found.png
public/assets/screens/02-partial.png
public/assets/screens/03-no-category.png
public/index.html
public/styles.css
scripts/blind-test.js
scripts/demo.js
scripts/preview.js
scripts/report.js
src/agent/llm.js
src/agent/parse.js
src/agent/router.js
src/ai/README.md
src/ai/blind.test.js
src/ai/cache.js
src/ai/demo.js
src/ai/explain.js
src/ai/explain.test.js
src/ai/scenarios.js
src/ai/templates.js
src/ai/verify.js
src/ai/warm-cache.js
src/ai/warm-cache.test.js
src/api/compare.js
src/api/selfcheck.js
src/engine/compare.js
src/engine/config.js
src/engine/data.js
src/engine/filters.js
src/engine/recommend.js
src/engine/score.js
src/server.js
tests/browser/site.spec.js
tests/compare.test.js
tests/contract.test.js
tests/engine.test.js
tests/selfcheck.test.js
tests/server.test.js

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

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

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