AAYMA
RU
Войти

Подбор подрядчиков под мероприятие

Сервис подбора подрядчиков для мероприятия: принимает город, дату, формат, категорию и бюджет (формой или свободным текстом в мини-чате) и…

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

О проекте

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

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

Суть

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

Как устроено

Мини-чат: LLM раскладывает запрос по перечням каталога, код проверяет членство, дату и бюджет, пользователь подтверждает разбор. Дальше 7 жёстких фильтров в фиксированном порядке (город → категория → формат → свободен на дату → бюджет → язык → длительность) с воронкой. Пожелания сверяются с описаниями через эмбеддинги text-embedding-3-small (256 измерений). Взвешенный скоринг с тай-брейком по id, различители карточек считаются кодом до генерации (3 яруса). Один вызов gpt-4.1-mini с temperature=0 на всю выдачу, затем валидатор (числа из фактов, пересечение ≤0.35, повтор или шаблон) и кэш по хешу. Каталог из 241 профиля лежит в JSON без БД.

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

  • hard-filter funnel
  • weighted scoring
  • deterministic tie-break
  • differentiators
  • constraint relaxation
  • text embeddings semantic matching
  • LLM output validator
  • hash-based explanation cache
  • golden-file regression tests

Метрики

ядро 0–12 мс, холодный ответ 1,9–2,5 с; максимальное пересечение объяснений 0.32 (порог 0.35); 12/12 эталонных сценариев PASS; 32 теста; в 84% комбинаций трёх свободных подрядчиков нет

Особенности

Правило «решает код, модель только формулирует». Различители считаются до генерации, чтобы объяснения нельзя было переставить местами. Валидатор бракует выдуманные числа, детерминизм обеспечивает кэш, а не temperature=0. Анализ данных показал, что лишь в 16% комбинаций находится ≥3 свободных подрядчика, отсюда воронка и послабления. Проверка без API-ключа: npm run verify.

Стек

Next.js 16 · TypeScript · Tailwind CSS · OpenAI API · Node.js

Модели

gpt-4.1-mini · text-embedding-3-small


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

README

README.md

Подбор подрядчиков под мероприятие — с объяснением, почему именно они

Трек «Креативные индустрии» · Кейс #79-lite «Умный подбор подрядчиков» · HackAlem AI 2026 Команда neIT.kz

🔍 Проверить без нашего API-ключа — одна команда: npm ci && npm run verify

Главный экран: подбор с объяснениями


Краткое описание

Проблема. Заказчик получил каталог подрядчиков по своему городу и не может выбрать. Каталог не врёт, но и не помогает: половина анкет на конкретную дату уже занята, а понять это можно только обзвоном.

Цифры из данных — посчитаны скриптом, и его можно запустить: npm run stats (ключ не нужен).

  • занято 50 дней из 100 у профиля в среднем, в декабре — 73% дней;
  • при переборе 1224 комбинаций «город × категория × формат × дата» (4 даты: 01.10, 14.11, 06.12, 12.12) три и более подходящих подрядчика находятся только в 16% случаев. В остальных 84% трёх карточек просто не существует: 32% дают одного-двух, 37% — ноль свободных, 16% — такой категории нет в городе.

Для кого. Клиент площадки-агрегатора, который уже выбрал город, дату и тип подрядчика.

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


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

Требования кейса

ТребованиеКак закрытоГде посмотреть
Вход: город, дата, тип мероприятия, категория, бюджет; опционально длительность и языкДва входа: свободный запрос в мини-чате (POST /api/parse) и форма (POST /api/match). Бюджет необязателен: без него фильтр по цене не применяется, а в ответе появляется оговоркаlib/parse.ts, app/api/match/route.ts
Выход: до 3 карточек с именем, категорией, городом, ценой и объяснением на 1–2 предложенияКарточки с объяснением, чипсами фактов и бейджами честности. Лимит трёх соблюдается на позицию: если клиент просит ведущего и фотографа, это два подбора по три карточки, а не шесть в одном спискеlib/explain.ts, app/page.tsx
Занятый на дату не попадает в выдачуШаг 4 фильтра; площадки идут тем же путём, что людиlib/filters.ts
Подходящих меньше трёх — показать сколько есть и почемуТекст исхода считается кодом с точными числами; блок послаблений «ещё может подойти, если…»; если не проходит никто — блок «ближайшее, что есть в каталоге» с честной величиной расхождения («дороже бюджета в 4,5 раза», «свободен на 5 дней позже»)lib/match.ts, lib/relax.ts
Детерминизм: тот же запрос — тот же порядокЧистые функции, округление score до 4 знаков, тай-брейк по id, кэш объясненийlib/scoring.ts
Три исхода различимы явноИсходов четыре: добавлен «формат не берёт никто» — в данных таких пар 47lib/types.ts

Definition of Done

Пункт DoDЧем закрытГде посмотреть
Ответ до 10 секундЯдро 0–12 мс; холодный ответ с объяснениями модели — 1,9–2,5 с на замере; повтор из кэша мгновенныйsamples/report.md
Объяснения не взаимозаменяемыРазличители считаются до обращения к модели; валидатор бракует пересечение > 0.35. Замерено на всех сценариях: максимум 0.32lib/differentiators.ts, samples/report.md
Повторный запуск — тот же порядокnpm run verify сверяет 12 сценариев с эталонами (12/12 PASS без ключа) + npm test — 32 теста, включая три прогона подряд на идентичностьsamples/golden/, tests/core.test.ts
Один запрос, две даты — разный результат, и видно, что дело в занятостиОдин и тот же «Ведущий · Алматы · корпоратив»: S1 на 01.10 — 3 карточки, заняты 8 из 17; S2 на 12.12 — 2 карточки, заняты 14 из 17. Разницу называет сам текст исходаsamples/golden/S1.json, S2.json
Плотная категория на осеннюю датуS1 «Ведущий · Алматы · корпоратив · 01.10» — в категории 21 профильsamples/golden/S1.json
Редкая категорияS4 «Флорист · Алматы» — в категории 4 профиля, в выдаче 1samples/golden/S4.json
Запрос без результата, объяснённый словамиS2, S7, S8 — три разных типа пустоты, у каждого свой текст с числамиsamples/golden/S7.json, S8.json
Команда объясняет, что внутри пайплайнаВоронка отсева возвращается в каждом ответе и рисуется на экранеполе funnel в любом файле samples/golden/

Как работает решение

Главный принцип: решения принимает детерминированный код, модель только формулирует. Всё, что модель написала, проверяется кодом против фактов.

  1. Приём запроса (мини-чат) — модель получает перечни каталога (3 города, 17 категорий, 6 форматов) и обязана вернуть значение ровно из списка, сама справляясь с падежами, регистром, опечатками и синонимами: «в АСтану» → Астана, «нужин вядущий на карпоратив» → Ведущий + корпоратив, «тамаду на тоййй» → Ведущий + той. Код затем проверяет членство в перечне, дату — на попадание в окно датасета, а цифры бюджета — на присутствие в тексте пользователя. Словарное сопоставление осталось подстраховкой и работает, когда ключа модели нет вовсе. Всё, чего в каталоге нет, возвращается с цитатой. Перед поиском пользователь подтверждает разбор — поиск идёт от подтверждённой структуры, а не от текста, поэтому разбор не влияет на детерминизм выдачи.
  2. Жёсткие фильтры — семь шагов в фиксированном порядке: город → категория → формат → свободен на дату → бюджет → язык → длительность. Каждый шаг записывает, сколько отсеял и почему. 1а. Семантика пожеланий — «украсит розами», «без пошлых конкурсов» нельзя сверить со списком, это открытое множество. Пожелание и предложения описаний превращаются в векторы (text-embedding-3-small, сжатие до 256 измерений), и пожелание считается подтверждённым, если ближайшее предложение достаточно близко по смыслу. Подтверждение всегда приносит цитату из профиля; неподтверждённое пожелание карточка обязана назвать вслух.
  3. Ранжирование — взвешенная сумма факторов; вес отдан тому, что различает кандидатов между собой, а не тому, по чему они прошли фильтр. Тай-брейк по id.
  4. Контракт фактов и различители — код заранее находит, чем карточки отличаются друг от друга, и раздаёт каждой свои отличия. Три яруса: поля анкеты → уникальная фраза из описания → честное признание, что профили почти совпадают.
  5. Объяснения — один вызов модели на всю выдачу, temperature = 0, на входе только факты.
  6. Валидатор (код, не промпт) — числа из текста обязаны присутствовать в фактах; запрещены общие фразы; пересечение объяснений не выше 0.35; неподтверждённое пожелание обязано быть названо вслух. Брак → один повтор → шаблон из фактов.
  7. Послабления — если карточек меньше трёх, отдельным блоком показываем варианты с оговоркой.

Почему так, а не иначе

Один вызов модели на всю выдачу, а не три отдельных. Если писать объяснения независимо, они получаются взаимозаменяемыми: модель не знает, чем карточка отличается от соседней. Видя все три сразу и получая готовые различители, она пишет тексты, которые нельзя переставить местами.

Различители считаются кодом до генерации. В данных есть тройка национальных ансамблей с одинаковой ценой, одинаковыми часами, одним языком и одним форматом — структурно они неразличимы. Для таких случаев есть второй ярус: уникальная фраза из описания («впервые выступивший перед главой государства»). И третий — честно сказать, что предложения почти совпадают, вместо выдуманного отличия.

Воронка возвращается в API, а не только рисуется. Она же вход для эталонных проверок и доказательство, что фильтры делают ровно то, что заявлено.

Кэш, а не обещание детерминизма. temperature = 0 не гарантирует одинаковый ответ у провайдера. Гарантию даёт кэш по хешу «запрос + факты + версия промпта + модель». Порядок карточек от модели не зависит вообще — его определяет код.


Технологии

  • Next.js 16 (App Router), TypeScript strict, Tailwind CSS
  • OpenAI API: gpt-4.1-mini для объяснений и разбора запроса (temperature = 0), text-embedding-3-small (256 измерений) для смыслового сравнения пожеланий с описаниями
  • Без базы данных: 241 профиль лежит в репозитории как JSON — осознанное решение ради воспроизводимости
  • AI-агент разработки: Claude Code

Архитектура проекта

браузер (app/page.tsx)
  ├─ POST /api/parse        разбор свободного запроса → поля + «правильно понял?»
  │    └─ lib/parse.ts      модель раскладывает, код проверяет по словарям каталога
  └─ POST /api/match
       ├─ lib/filters.ts        7 жёстких фильтров + воронка + исход
       ├─ lib/scoring.ts        веса, факты, пожелания, тай-брейк по id
       ├─ lib/differentiators.ts чем карточки отличаются друг от друга (3 яруса)
       ├─ lib/relax.ts          послабления: дата, формат-сосед, перелёт, бюджет, часы
       ├─ lib/match.ts          сборка ответа + тексты исходов (считаются кодом)
       └─ lib/explain.ts        один вызов модели → валидатор → кэш → шаблон
data/contractors.json   241 профиль, списки нормализованы в множества
data/scenarios.json     единый источник сценариев: интерфейс, проверка, отчёт
samples/golden/         эталонные ответы на все сценарии

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

git clone <repo> && cd hack-40ac7a06-neit-kz
npm ci
npm run verify     # проверка всех сценариев, ключ НЕ нужен
npm test           # 32 теста: детерминизм, инварианты, разбор запроса, семантика, проезд
npm run stats      # числа из README, посчитанные по каталогу
npm run dev        # http://localhost:3000

.env.local необязателен. Без OPENAI_API_KEY сервис работает: объяснения берутся из закоммиченного кэша, а при промахе кэша собираются шаблоном из фактов. Порядок и состав карточек при этом те же — их определяет код, а не модель. Чтобы получать живые объяснения, впишите свой ключ:

cp .env.example .env.local   # OPENAI_API_KEY=sk-...

Как проверить решение

Путь 1 — без браузера, 30 секунд

npm run verify

Ожидаемый вывод:

PASS            S1  MATCHED              карточек 3+0+0  11 мс  (cache)
PASS            S2  MATCHED              карточек 2+0+0  1 мс  (cache)
PASS            S3  MATCHED              карточек 3+0+0  1 мс  (cache)
PASS            S4  MATCHED              карточек 2+0+0  1 мс  (cache)
PASS            S5  MATCHED              карточек 2+1+0  0 мс  (cache)
PASS            S6  MATCHED              карточек 2+1+0  1 мс  (cache)
PASS            S7  NO_ONE_PASSES        карточек 0+0+3  1 мс  (cache)
PASS            S8  NO_FORMAT_IN_POOL    карточек 0+0+3  0 мс  (cache)
PASS            S9  MATCHED              карточек 2+1+0  10 мс  (cache)
PASS            S10  NO_ONE_PASSES        карточек 0+0+3  0 мс  (cache)
PASS            S11  MATCHED              карточек 1+2+0  1 мс  (cache)
PASS            S12  MATCHED              карточек 2+1+0  0 мс  (cache)
максимальное пересечение объяснений: 0.32 (порог 0.35)
все сценарии совпали с эталонами

Тайминги на вашей машине будут другими — в эталоны они не входят, сверяется только содержимое.

Путь 2 — в браузере, 2 минуты

npm run dev → http://localhost:3000 → кнопки-пресеты сверху:

НажатьЧто увидите
S13 карточки; в воронке видно, что из 17 ведущих, берущих корпоратив, 8 заняты 1 октября
S2тот же запрос на 12 декабря: карточек уже 2, потому что заняты не 8, а 14 из тех же 17 — декабрь плотнее
S3три структурно одинаковых ансамбля — и три разных объяснения
S5двое проходят, а третий занят в день свадьбы и свободен накануне: монтаж сдвигается, дата мероприятия — нет
S6в Астане подходящих всего двое, третьим идёт лайв-бэнд из Алматы — с ценой проезда рядом
S9«украсит розами» — в профилях роз нет, и система говорит об этом прямо
S10бюджет 400 000 ₸ на декоратора при ценах 600 000 – 2 200 000 ₸: не проходит никто, но экран не пустой — показаны ближайшие с расхождением «дороже бюджета на 200 000 ₸», «в 2 раза», «в 3 раза»
S11проходит только один; двое готовы приехать из Алматы, и рядом стоит цена проезда — 256 500 ₸ на пятерых, это больше остатка бюджета
S12декоратору из другого города не предлагают прилететь: он везёт конструкции, а не летит в кресле — вместо перелёта ему сдвигают монтаж

Проверка мини-чата (поле «Опишите мероприятие своими словами»):

Что написатьЧто произойдёт
нужен ведущий на свадьбу в Алматы 18 ноября, бюджет до миллиона, чтобы вёл на казахскомвсе поля разобраны, пожелания вынесены отдельно, кнопка «Всё верно, искать» активна
хочу ведущего из Шымкента на той 5 октября за 700 тысячкатегория, формат, дата и бюджет разобраны, а про Шымкент сказано прямо: такого города в каталоге нет. Ближайший город не подставляется
нужен фотограф недорого«недорого» не считается бюджетом; система спрашивает город и дату

Любой сценарий открывается прямой ссылкой: http://localhost:3000/?s=S10.

Три исхода на скриншотах:

Есть из кого выбратьСтруктурные близнецыНе подходит никто
S1S3S10

Два тумблера рядом с кнопкой поиска:

  • «стереть имена» — имена заменяются на «Подрядчик A/B/C». Карточки всё равно различимы: это буквальная проверка пункта DoD.
  • «объяснения моделью» — выключите, и объяснения соберутся шаблоном из фактов. Состав и порядок карточек не изменятся: видно, что ранжирование не зависит от модели.

Данные и интеграции

  • Каталог: 241 профиль. 66 — поставка организаторов без единой правки, включая календари занятости; 175 — синтетические профили, дописанные нашей командой в том же формате (hackathon_dataset_241.csv в корне репозитория).
  • Где чей профиль — видно в карточке. Наши помечены бейджем «профиль дописан нашей командой» (поле addedByTeam), синтетические из поставки — «синтетический профиль из поставки» (HK-90001…HK-90013). Требование кейса «в демо должно быть видно, где настоящий профиль, а где ваш» выполняется явной меткой, а не молчанием.
  • ⚠️ Важная деталь. В присланном расширенном файле календари исходных 66 профилей оказались пересобраны на год вперёд. Мы это откатили: поставка используется ровно в том виде, в каком пришла, а из расширенного файла взяты только 175 профилей, которых в поставке не было. Проверяется командой npm run stats — строка «в среднем занято дней»: по поставке ровно то же число, что в исходном CSV.
  • ⚠️ Известная помарка. У дописанных профилей календарь шире окна кейса — часть занятых дат приходится на 2027 год (npm run stats показывает их числом). На подбор это не влияет: запрос ограничен окном 23.09 — 31.12.2026 и проверяет одну конкретную дату. Чистить перед сдачей не стали намеренно — поле входит в ключ кэша объяснений, и правка потребовала бы перегенерации кэша, то есть сломала бы проверку без ключа.
  • Честность полей: у 18 профилей цена помечена как оценочная (price_imputed) — среди них все банкетные залы, рестораны, загородные площадки и отели. В карточке стоит бейдж «цена ориентировочная».
  • Ловушки данных, которые мы учли: порядок значений в списках не нормализован (русский|казахский и казахский|русский встречаются поровну) — парсим в множества; max_hours = null означает «работа не привязана к присутствию» (флорист, декоратор, сувениры — 28 профилей), и фильтр длительности их не отсеивает; в поставке у 25 профилей бренд в описании не совпадает с именем — модели запрещено брать имя из текста.
  • Внешние сервисы: OpenAI API (объяснения и разбор запроса) и снимок цен на перелёты из Travelpayouts / Aviasales — см. раздел «Стоимость проезда» ниже. Живых вызовов в рантайме нет: и объяснения, и цены перелётов лежат в репозитории.

Ограничения

  • Каталог ограничен поставкой: 3 города, 17 категорий, окно дат 23.09–31.12.2026. Вне окна busy_dates пуст, и выдача неинформативна.
  • Совпадение с пожеланиями ищется словарно по описаниям. Описания короткие (медиана 313 символов), поэтому сигнал слабый — мы предпочли честное «в профиле не упомянуто» выдуманному совпадению. Семантический поиск на эмбеддингах в эту версию не вошёл.
  • Мини-чат разбирает запрос одним проходом: многоходового диалога с накоплением контекста нет — недостающее дозаполняется в форме. Разбор кэшируется в памяти процесса, а не на диске.
  • Без ключа модели мини-чат не работает (об этом говорит сообщение в интерфейсе); подбор, объяснения из кэша и npm run verify при этом работают полностью.
  • Бронирование, заявки и уведомления подрядчику — вне объёма кейса.
  • Интерфейс минимальный: по условию кейса приоритет у качества объяснений, а не у оформления.

Семантика пожеланий и работа без ключа

Что делает модель, а что код. Падежи, опечатки и синонимы разбирает модель — она получает перечни каталога и обязана вернуть значение ровно из списка. Смысловое сравнение пожеланий с описаниями — эмбеддинги. А решения принимает код: фильтры, ранжирование, порог подтверждения, воронка.

Порог подтверждения — 0.42, выбран замером, а не на глаз. Проверяли на реальных описаниях:

ПожеланиеБлижайшее по смыслуДолжно подтвердиться
«репортажная съёмка без постановки»0.674да
«зал на 500 человек с парковкой»0.638да
«ведёт на казахском языке»0.575да
«цветочное оформление свадьбы»0.566да
«без пошлых конкурсов»0.563да
«живая музыка на праздник»0.472да
«украсит розами»0.408нет — роз нет ни в одном описании
«нужен гипноз и фокусы»0.386нет
«доставка дронами»0.335нет

Таблица пересчитывается командой npx tsx scripts/calibrate-wishes.ts. Между 0.408 и 0.472 граница и проходит. Показательно, что семантика не сломала наш главный тест на честность: слово «розы» в каталоге не встречается, и система по-прежнему говорит об этом прямо, вместо того чтобы притянуть «работаем с сезонными и привозными цветами».

Почему это не мешает проверке без ключа. Векторы посчитаны заранее и лежат в репозитории:

  • data/embeddings.json — 1070 предложений из 241 описания (2,1 МБ);
  • data/wish-embeddings.json — векторы уже встречавшихся пожеланий, включая пожелания демо-сценариев.

Поэтому npm run verify у проверяющего считает ровно те же числа, что и у нас: совпадение пожеланий влияет на порядок карточек, и расхождение здесь сломало бы детерминизм. Если ввести своё пожелание, которого нет в кэше, и ключа при этом нет — система честно падает на словарное сравнение и пишет об этом в карточке: «по словам, семантика без ключа недоступна».

Пересчитать векторы можно командой npm run build:embeddings — она нужна только нам, проверяющему нет.

Стоимость проезда подрядчика из другого города

Когда в городе клиента подходящих нет, мы предлагаем подрядчика из соседнего города — и сразу показываем, во сколько обойдётся его приезд. Без этого числа клиент не может принять решение.

Статус: это демонстрация механики, а не подключённый источник цен. Токена партнёрской программы Travelpayouts у нас нет и на хакатоне мы его не получали. Цены лежат ручным снимком в data/flights.json (поле source это фиксирует), и точные значения есть только на даты демо-сценариев — остальные дни честно обслуживаются медианой месяца, и в карточке видно, что сработала медиана.

Как это выглядит в полноценной реализации. Берём токен партнёрской программы, добавляем npm run refresh:flights — восемь запросов к month-matrix (4 месяца × 2 направления) обновляют снимок, либо переходим на живой вызов с кэшем на несколько часов. Врезка одна — тело estimateTravelCost(), интерфейс функции не меняется, поэтому ни правила, ни карточка, ни тесты от этого не пострадают.

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

Как считается. Сумма двух разных плеч — туда и обратно — умноженная на состав. Если цены на нужный день в снимке нет, берётся ближайший день в пределах трёх суток, затем медиана месяца; какой вариант сработал, видно в карточке.

Проверка проекта от этого не зависит: npm run verify и npm run dev работают без каких-либо токенов — снимок уже в репозитории.

Допущения, названные прямо (их же видит пользователь в карточке):

  • состав считается по таблице: ведущий — 1 человек, лайв-бэнд — 5. Поля о составе в анкете нет, это наше допущение. Категория, которой в таблице нет, проезд не получает вовсе — молча посчитать танцевальный коллектив как одного человека хуже, чем не считать;
  • тариф минимальный: без багажа (для бэнда это инструменты) и без проживания — по факту выйдет дороже;
  • считаем самолёт; ночной поезд Алматы — Астана дешевле в 2–3 раза, но источника по нему у нас нет;
  • предполагаем, что проезд не входит в гонорар, хотя поля об этом в анкетах нет;
  • порог правила «предлагать из другого города» — гонорар от 1 млн ₸. Снижать его нельзя: потребовался бы состав ещё для четырёх категорий, которых в данных нет.

Статус фичи честно: стоимость проезда не закрывает ни один пункт Definition of Done — это работа сверх требований кейса.

Два решения, отменённых расчётом (оставлены здесь, потому что отменённое решение с расчётом полезнее нереализованного пункта):

  • отсечка «гонорар + проезд > бюджета в 1,5 раза» недостижима на этом каталоге: правило требует гонорар ≤ бюджета и гонорар ≥ 1 млн, значит условие сводится к «проезд дороже 500 000 ₸» — вдвое выше максимума по снимку;
  • сортировка по полной стоимости тождественна текущей: внутри блока у всех кандидатов одна категория, один состав, один город и одна дата, то есть один и тот же проезд.

Карточка с ценой проезда

Два сценария внедрения

Решение не привязано к нашему каталогу — это надстройка над любой базой подрядчиков. Страница /integration показывает оба способа вживую: слева макет чужой площадки с нашим виджетом внутри, справа — код подключения.

1. Виджет в <iframe> — две строки разметки.

<iframe src="https://.../embed?city=Алматы&category=Ведущий"
        style="width:420px;height:640px;border:1px solid #e2e8f0;border-radius:12px"></iframe>

Параметры city, category, format, date и budget передают то, что площадка уже знает о клиенте. Если переданы город, категория и дата — виджет показывает подбор сразу, без вопросов. На бэкенде площадки менять ничего не нужно.

Виджет отдаёт результат наружу. После подбора он шлёт родительской странице сообщение postMessage({type: 'podbor:match', cards, outcome, message, request}), и площадка может показать подборку своими средствами. На /integration это видно вживую: каталог слева перестраивается и показывает подходящих с собственной кнопкой «Написать» — карточки и кнопка принадлежат площадке, от нас приходят только данные и объяснения.

Несколько позиций в одном запросе. «Нужен ведущий и фотограф на свадьбу, ведущему 900 тысяч, фотографу 300» даёт две подборки со своими бюджетами, а не одну усреднённую. Разбор идёт в два прогона (POST /api/brief):

  1. Прогон «сколько позиций» — модель называет только список категорий, без деталей.
  2. Прогон на каждую позицию — свой промпт, который видит только свою категорию, и потому не путает бюджеты и пожелания между позициями. Позиции разбираются параллельно.

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

Общий бюджет делится по медианным ценам каталога. Клиент обычно называет одну сумму на всё мероприятие. Делить её поровну бессмысленно — ведущий и лайв-бэнд стоят по-разному. Мы считаем медианную цену каждой категории по каталогу и делим пропорционально: «3 000 000 ₸ на той в Астане» превращается в 1 110 000 ₸ ведущему, 270 000 ₸ фотографу и 1 600 000 ₸ лайв-бэнду. Деление показывается в карточке подтверждения, и его можно перебить, назвав суммы по позициям.

Приём взят из AI-парсера CastHub (countRoles → разбор каждой роли отдельными вызовами): один большой промпт на mini-модели путает поля между сущностями.

2. Чат-бот, ведущий предварительный диалог. Это и есть содержимое виджета (/embed): бот спрашивает недостающее по одному пункту, накапливая контекст между репликами, и, как только собраны город, категория и дата, сам показывает подбор с объяснениями.

— нужен фотограф недорого
— В каком городе? В каталоге есть Алматы, Астана и Зарубежье.
— в Алматы
— На какую дату?
— 14 ноября, на свадьбу
— Какой у вас бюджет? Если не важно — можно искать без него.
— бюджет 400 тысяч, хочу репортажную съёмку
→ Фотограф · Алматы · свадьба · 14.11.2026 · до 400 000 ₸ · пожелания: недорого, репортажная съёмка

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

3. По API для площадок со своим интерфейсом. POST /api/match отдаёт карточки, воронку отсева и тексты исходов; POST /api/parse разбирает свободный запрос и держит диалог — предыдущее состояние передаётся полем known.

Ценность, применимость и потенциал

Кому решение подходит, что нужно для внедрения, экономика и планы развития — отдельным документом: docs/business.md.

Коротко: это надстройка над любой базой подрядчиков — маркетплейсы услуг, свадебные порталы, корпоративные event-агентства. Нужен только доступ к профилям через API или прямую интеграцию, переделывать каталог не требуется. Ближайший шаг развития — админка площадки, где веса факторов подбора и тексты промптов настраиваются под конкретный рынок без правки кода.

Ссылка на развёрнутую версию

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

Команда

  • Мади Жумабек — архитектура решения, ядро подбора, объяснения и валидатор, README.
Звёзды
⭐ 0
Форки
0
Язык
TypeScript
Последний push
в прошлом месяце
Коммиты
40
Авторы
2
Файлы
90
Ветки
1

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

Madi Zhumabek 37alimbacat 3

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

  • 2026-09-23 17:46docs: README сверен с фактическим поведением на каталоге 241Madi Zhumabek
  • 2026-09-23 17:35ui: даты человеческим видом — «15 декабря» вместо 2026-12-15Madi Zhumabek
  • 2026-09-23 17:30chore: референс по дизайну переехал в docs/Madi Zhumabek
  • 2026-09-23 17:29docs: три сценария сметы вместо двух — измерено на каталоге 241Madi Zhumabek
  • 2026-09-23 17:28docs: тесты сметы на инвариантах, а не на числах каталогаMadi Zhumabek
  • 2026-09-23 17:25feat: расширенный каталог — 241 профиль, наши помечены отдельноMadi Zhumabek
  • 2026-09-23 17:23docs: план реализации сметы — 10 задач по TDDMadi Zhumabek
  • 2026-09-23 17:21docs: спека сметы — комбинации подрядчиков по нескольким позициямMadi Zhumabek
Все коммиты (40)
  • 2026-09-23 17:20Расширенные датасетalimbacat
  • 2026-09-23 16:48docs: README — актуальные измерения вместо устаревшихMadi Zhumabek
  • 2026-09-23 16:42feat: общий бюджет делится по медианам каталога; воронка сворачивается; карточки в сеткуMadi Zhumabek
  • 2026-09-23 16:31fix: многопозиционный разбор и на главной странице, не только в виджетеMadi Zhumabek
  • 2026-09-23 16:27feat: разбор в два прогона — сколько позиций, потом каждая отдельноMadi Zhumabek
  • 2026-09-23 16:16feat: виджет отдаёт подборку площадке, каталог перестраивается под критерииMadi Zhumabek
  • 2026-09-23 16:09chore: промпты доступны для ревью через npm-скриптMadi Zhumabek
  • 2026-09-23 16:07feat: два сценария внедрения — виджет в iframe и чат-бот с диалогомMadi Zhumabek
  • 2026-09-23 15:59ui: косметика интерфейса — читаемость вместо отладочного видаMadi Zhumabek
  • 2026-09-23 15:40ui: человеческая подпись исхода вместо технического кодаMadi Zhumabek
  • 2026-09-23 15:37feat: семантика пожеланий на эмбеддингах, с работой без ключаMadi Zhumabek
  • 2026-09-23 15:30refactor: падежи и опечатки разбирает модель, код проверяет членство в перечнеMadi Zhumabek
  • 2026-09-23 15:27fix: подстраховка разбора — ищем поля в исходном тексте, если модель их не вернулаMadi Zhumabek
  • 2026-09-23 15:27Референс по дизайнуalimbacat
  • 2026-09-23 15:26fix: разбор запроса — падежи, шум в замечаниях и дубли ключейMadi Zhumabek
  • 2026-09-23 15:18docs: честный статус цен на проезд — демонстрация, а не подключённый источникMadi Zhumabek
  • 2026-09-23 15:16feat: стоимость проезда подрядчика из другого городаMadi Zhumabek
  • 2026-09-23 15:10docs: спека перелёта — тестовый слой уже существуетMadi Zhumabek
  • 2026-09-23 15:09docs: бизнес-раздел в docs/business.md и ссылка из READMEMadi Zhumabek
  • 2026-09-23 15:09docs: спека перелёта v3 — по итогам двух кругов ревьюMadi Zhumabek
  • 2026-09-23 15:09test: 11 тестов детерминизма и инвариантов + скриншоты в READMEMadi Zhumabek
  • 2026-09-23 15:06СЕО-биз-ценность.mdalimbacat
  • 2026-09-23 14:58feat: блок «ближайшее, что есть в каталоге» когда не проходит никтоMadi Zhumabek
  • 2026-09-23 14:53docs: README про мини-чат — раздел «что реализовано», архитектура, проверкаMadi Zhumabek
  • 2026-09-23 14:52feat: мини-чат на входе — разбор свободного запроса с проверкой кодомMadi Zhumabek
  • 2026-09-23 14:47feat: проверка без ключа + README по структуре организаторовMadi Zhumabek
  • 2026-09-23 14:45ui: человекочитаемые подписи источника отличияMadi Zhumabek
  • 2026-09-23 14:44docs: спека стоимости перелёта в правиле FLY_INMadi Zhumabek
  • 2026-09-23 14:41feat: объяснения с валидатором, интерфейс с пресетами и воронкойMadi Zhumabek
  • 2026-09-23 14:36feat: ядро подбора — фильтры с воронкой, скоринг, различители, релаксацияMadi Zhumabek
  • 2026-09-23 14:11chore: фиксируем LF в репозиторииMadi Zhumabek
  • 2026-09-23 14:11docs: спека решения v2 (кейс #79-lite, подбор подрядчиков)Madi Zhumabek
Структура репозитория (90 файлов)
.env.example
.gitattributes
.gitignore
README.md
app/api/brief/route.ts
app/api/match/route.ts
app/api/parse/route.ts
app/embed/page.tsx
app/favicon.ico
app/globals.css
app/integration/page.tsx
app/layout.tsx
app/page.tsx
data/contractors.json
data/embeddings.json
data/explanations-cache.json
data/flights.json
data/meta.json
data/scenarios.json
data/wish-embeddings.json
docs/business.md
docs/screen-S1.png
docs/screen-S10.png
docs/screen-S11.png
docs/screen-S3.png
docs/spec.md
docs/superpowers/plans/2026-09-23-smeta.md
docs/superpowers/specs/2026-09-23-flight-costs-design.md
docs/superpowers/specs/2026-09-23-smeta-design.md
docs/superpowers/specs/2026-09-23-ui-cosmetics-design.md
docs/Референс по дизайну.html
eslint.config.mjs
hackathon_dataset_241.csv
lib/brief.ts
lib/catalog.ts
lib/dates.ts
lib/differentiators.ts
lib/embeddings.ts
lib/explain.ts
lib/filters.ts
lib/match.ts
lib/parse.ts
lib/relax.ts
lib/scoring.ts
lib/travel.ts
lib/types.ts
lib/wish-vectors.ts
next.config.ts
package-lock.json
package.json
postcss.config.mjs
public/file.svg
public/fonts/onest-cyrillic.woff2
public/fonts/onest-latin-ext.woff2
public/fonts/onest-latin.woff2
public/globe.svg
public/next.svg
public/vercel.svg
public/window.svg
samples/golden/S1.json
samples/golden/S10.json
samples/golden/S11.json
samples/golden/S12.json
samples/golden/S2.json
samples/golden/S3.json
samples/golden/S4.json
samples/golden/S5.json
samples/golden/S6.json
samples/golden/S7.json
samples/golden/S8.json
samples/golden/S9.json
samples/report.md
scripts/build-embeddings.ts
scripts/calibrate-wishes.ts
scripts/demo.ts
scripts/print-prompts.ts
scripts/stats.ts
scripts/try-brief.ts
scripts/try-dialog.ts
scripts/try-dialog2.ts
scripts/try-explain.ts
scripts/try-near.ts
scripts/try-parse.ts
scripts/try-travel.ts
scripts/verify.ts
tests/core.test.ts
tests/parse.test.ts
tests/travel.test.ts
tests/wishes.test.ts
tsconfig.json

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

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

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