Таңда
Сервис подбирает до трёх свободных event-подрядчиков под параметры заказа и объясняет выбор и порядок фактами из профилей.
О проекте
Команда: 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.
«Таңда» по-казахски — «выбирай». Сервис не просто сортирует каталог: он показывает до трёх подрядчиков и словами объясняет, почему именно они и почему в таком порядке.
Проблема
Заказчик открывает каталог площадки и видит длинный список карточек, одинаковых по виду: цена «от», список форматов, несколько строк о себе — и никакого ответа на вопрос, кого из них брать и чем один лучше другого. В сезон список сжимается ещё сильнее: по данным кейса в декабре подрядчик занят в среднем 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; ядро не знает, что речь о мероприятиях.
Метрики пилота
Что измеряем на площадке-партнёре, чтобы решение было видно в цифрах, а не на словах:
- конверсия показанной выдачи в отправленную заявку;
- доля пустых выдач и какая их часть заканчивается принятой альтернативой (другая дата, бюджет, город);
- время от запроса до выбора подрядчика;
- доля запросов, где пользователь открыл объяснение или раскрыл причины отсева.
Базовые значения снимаются на текущей выдаче площадки до подключения — без этого сравнение бессмысленно.
Дорожная карта
- Заявка и бронирование прямо из карточки вместо перехода в мессенджер.
- Синхронизация календарей подрядчиков, чтобы занятость обновлялась сама, а не выгрузкой.
- Объяснения на казахском языке наравне с русским.
- Отзывы и подтверждённые заказы как ещё один источник фактов для объяснений.
- Соседние ниши на том же ядре: репетиторы, клиники, строительные бригады.
Как выглядит
| Подобрали трёх | Осталась одна | Категории нет в городе |
|---|---|---|
![]() | ![]() | ![]() |
Как запустить
Работает без ключа 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 ₸ | found | 3 карточки: «Из 10 подрядчиков категории «Ведущий» в Алматы подходят ровно 3 — показываем всех.» |
| Тот же запрос на 26.12.2026 | partial | 1 карточка: «…подходит только 1: 9 заняты 26.12.2026.» |
| Флорист, Алматы, свадьба, 17.10.2026, 500 000 ₸ | partial | 1 из 2, второй занят; профиль помечен как синтетический |
| Банкетный зал, Алматы, корпоратив, 19.12.2026, 3 000 000 ₸ | all_filtered | 0 карточек: «…не подходит ни один: 7 заняты 19.12.2026. Ближайшая подходящая дата — 16.12.2026.» |
| Декоратор, Астана, свадьба, 17.10.2026, 1 000 000 ₸ | no_category | 0 карточек: «В Астане нет подрядчиков категории «Декоратор». В Алматы — 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:
| Проверка | Результат |
|---|---|
| Детерминизм демо №1 | HK-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.js | Express-приложение, таймауты, обработка ошибок | участник 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.
Авторы коммитов
История коммитов
- 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 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.


