О проекте
Команда: iwbhappy
Кейс HackAlem.ai: Торговля — Электрокомплект · ekt.kz
Стадия: Доведённый продукт
Задача кейса: AI-консультант по каталогу ekt.kz: товары, аналоги, цены, остатки, корзина
Суть
Чат-консультант по каталогу электротехники ekt.kz (300 реальных позиций из API партнёра). Отвечает про наличие, характеристики, сертификаты, аналоги с обоснованием и условия покупки, кладёт товар в корзину только после явного согласия и даёт ссылку на корзину.
Как устроено
Серверный цикл tool-calling в route handler Next.js с 9 инструментами (get_product, find_alternatives, get_terms, confirm_add, get_cart_link, find_related и др.) поверх SQLite/Turso, ответ стримится через SSE. Модель только предлагает товар; реплику клиента на согласие проверяет сервер, предложение одноразовое и ограничено остатком. Вложения Excel/Word/PDF/фото разбираются отдельно (фото и PDF через vision). Без ключа работает записанный mock-режим с роутингом по намерению.
Ключевые решения
- function calling
- SSE-стриминг
- серверная проверка согласия
- одноразовые предложения
- vision для накладных
- маскирование PII
- определение языка (kk/ru)
Метрики
92 детерминированные smoke-проверки, 22 реплики mock-check; ход агента на живой модели 2–4 с
Особенности
Подтверждение корзины проверяет сервер, а не промпт: вопрос или условие («если есть, добавь») не считаются согласием. Ссылка на корзину отдаёт токен только для чтения. Есть embed.js-виджет для чужого сайта и закладка для ekt.kz, поддержка казахского и эскалация на менеджера.
Стек
TypeScript · Next.js 15 · React 19 · SQLite (@libsql/client) · Turso · Vercel · zod
Модели
gpt-4.1-mini (OpenAI) · gpt-4.1-nano (fallback)
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-0af00165-iwbhappy.
README
README.mdИИ-ассистент для чата ekt.kz
Прототип для HackAlem AI, трек «Торговля». Кейс партнёра — ТОО «Электрокомплект» (ekt.kz).
Консультант в одном окне чата на сайте: отвечает по каталогу электротехники — наличие, технические характеристики, сертификаты, аналоги, условия покупки — и добавляет товары в корзину только после явного подтверждения клиента. Последний шаг — ссылка на корзину; заказ клиент оформляет сам.
- Настоящие данные. 300 позиций из API ekt.kz: артикулы, цены, характеристики, остатки по складам, рекомендации партнёра. Условия покупки — с публичных страниц ekt.kz. Синтетические только сертификаты, и они помечены.
- Подтверждение проверяет сервер, а не промпт. Модель может только предложить товар. Добавление исполняется после проверки реплики клиента на согласие — одноразово и не больше остатка.
- Работает без сети. Нет ключа или интернета — записанный режим проходит
сценарий, и интерфейс честно помечает его бейджем
MOCK.
Попробовать: витрина с чатом · чужой сайт с виджетом · закладка для ekt.kz
Проверка за 5 минут
Нажмите «Новый диалог». Демо-артикулы берутся из каталога (npm run demo
печатает текущие): R9F12110 — в наличии, 404029х — без остатка
(последняя буква — кириллическая «х»; латинская «x» тоже найдётся).
- Наличие, характеристики, сертификат. «Расскажи про артикул R9F12110: есть в наличии, какие характеристики, есть ли сертификат?» Ожидается карточка из каталога: 558 шт., 2 295 ₸, номинальный ток 10 А и сертификат с явной отметкой «демонстрационный».
- Аналог с обоснованием. «А 404029х есть? Если нет — что посоветуешь взамен и почему?» Ожидается нулевой остаток и хотя бы один аналог в наличии; ответ объясняет, что совпадает и чем аналог отличается.
- Условия покупки. «Какие условия оплаты и доставки?» Ожидается ответ по условиям с сайта ekt.kz: способы оплаты, стоимость и сроки доставки, когда нужен менеджер.
- Добавление с подтверждением. «Добавь R9F12110 в корзину, 2 штуки». Появляется карточка-предложение, корзина остаётся пустой. Затем «Да, добавь» или кнопка «Да» — в корзине 2 шт. на 4 590 ₸. Запрос сверх остатка сервер урежет до доступного, и агент об этом скажет.
- Актуальная корзина. «Открыть страницу корзины» — те же позиция
и количество. В адресе
/cart?c=…токен только для чтения, а не идентификатор сессии. - Попытка обойти подтверждение. В новом диалоге: «Добавь R9F12110 в корзину, 1 штуку» — карточка, корзина пуста. «А можно добавить?» — это вопрос, корзина пуста. «Если есть на складе, добавь» — это условие, корзина пуста. «Да» — позиция добавлена. Карточка после этого теряет кнопки, а повторное подтверждение того же предложения сервер отклонит: оно одноразовое (smoke, раздел 4).
Соответствие ТЗ
| Пункт ТЗ | Где реализовано | Чем проверяется |
|---|---|---|
| §7.1 Наличие, характеристики, сертификат | get_product в src/lib/agent/tools.ts | npm run smoke, раздел 1 |
| §7.2 Аналог с обоснованием | find_alternatives в src/lib/agent/tools.ts, подбор и reason в src/lib/db.ts | smoke, раздел 2 |
| §7.3 Условия покупки | get_terms в src/lib/agent/tools.ts, data/terms.json с источниками ekt.kz | smoke, раздел 3 |
| §7.4 Корзина только после согласия, количество не больше остатка | confirm_add в src/lib/agent/tools.ts, consumeProposal в src/lib/db.ts, кнопка в src/app/api/confirm/route.ts | smoke, раздел 4: три проверки согласия, остаток, повтор |
| §7.5 Ссылка на актуальную корзину | get_cart_link в src/lib/agent/tools.ts, токен для чтения в src/lib/db.ts, src/app/cart/page.tsx | smoke, раздел 5; на проде — сквозной прогон «товар → да → страница корзины» |
| §5 Вход: Excel, Word, PDF и фото | src/lib/spec-parse.ts, src/lib/llm/vision.ts, src/app/api/upload/route.ts | smoke, разделы 5г и 6; файлы scripts/fixtures/ для ручной загрузки, PDF и фото требуют модели |
| §5 Задержка — единицы секунд | стриминг ответа, один вызов модели на шаг | ход агента на живой модели — 2–4 с |
| §8 Казахский язык | detectLang в src/lib/agent/loop.ts, матчер в src/lib/agent/tools.ts, src/components/i18n.ts | smoke, разделы 0 и 5а; npm run mock-check; с ключом: npm run demo -- --live --kk |
| §8 Сопутствующие товары | find_related в src/lib/agent/tools.ts, RECOMMEND партнёра в src/lib/db.ts, подсказки в src/components/AgentApp.tsx | smoke, раздел 5б; вручную — подтвердить товар 027004 кнопкой |
| §8 История диалога | localStorage в src/components/AgentApp.tsx, в пределах сессии | вручную: переписка → перезагрузка → история на месте |
| §8 Эскалация на менеджера | get_terms(contacts) в src/lib/agent/tools.ts, правило 9б в src/lib/agent/prompts.ts | smoke, раздел 3; npm run mock-check |
| §9 Защита корзины | src/lib/agent/tools.ts, src/lib/db.ts: токен ссылки, три проверки согласия, одноразовое предложение | smoke, разделы 4 и 5 |
| §9 Приватность | src/lib/redact.ts: карты, CVV, IBAN; src/lib/logger.ts: маскирование контактов в журнале | smoke, раздел 5в |
| §9 Объяснимость | поле reason у аналогов и сопутствующих в src/lib/db.ts | smoke, разделы 2 и 5б |
| §9 Совместимость с сайтом | public/embed.js, src/app/demo-site/page.tsx, src/app/try/page.tsx | вручную: /demo-site, закладка с /try |
| §9 Десктоп и мобильная версия | медиазапросы в src/app/globals.css | вручную на телефоне, в том числе ширина 375px |
| §10 Репозиторий и README | этот файл: архитектура, запуск, данные | — |
У ekt.kz нет открытой интеграции авторизации: история хранится в браузере в пределах сессии (до 40 элементов ленты, без миниатюр фотографий). История для авторизованных клиентов — план интеграции: нужна подписанная передача id пользователя с сервера Bitrix. Сейчас она не реализована.
Запуск
Нужен Node.js 20 или новее (проверено на 22).
git clone https://github.com/BAITC-Hacks/hack-0af00165-iwbhappy.git
cd hack-0af00165-iwbhappy
npm install
npm run dev
Открыть http://localhost:3000. Ключ к модели не обязателен: без него приложение работает в записанном режиме и проходит весь сценарий.
С живой моделью — создать .env.local:
LLM_MODE=auto
OPENAI_API_KEY=<ключ>
LLM_MODEL=gpt-4.1-mini
Переменные окружения
| Переменная | Назначение | По умолчанию |
|---|---|---|
LLM_MODE | auto — модель, при сбое записанный режим; live — только модель; mock — только записанный | auto |
OPENAI_API_KEY | ключ модели; живёт только на сервере | — |
LLM_MODEL | основная модель | gpt-4.1-mini |
LLM_FALLBACK_MODEL | запасная модель при rate limit | gpt-4.1-nano |
OPENAI_BASE_URL | совместимый прокси, если доступ выдадут на площадке | — |
DB_URL, DB_AUTH_TOKEN | база: file:… локально, libsql://… — удалённая (Turso) | file:./.data/hack.db |
TURSO_DATABASE_URL, TURSO_AUTH_TOKEN | то же; их подставляет интеграция Turso в Vercel | — |
EKT_API_BASE, EKT_API_USER, EKT_API_PASSWORD | только для npm run seed — перевыгрузки каталога | — |
Ключи и пароль к API партнёра — только в .env.local, он в .gitignore.
В репозитории лежит .env.example: значения по умолчанию заполнены,
ключи и пароли — пустые.
Проверки
npm run verify
Одна команда перед сдачей: проверка типов → smoke → demo → mock-check. Останавливается на первой ошибке. Demo и mock-check работают в записанном режиме без вызова внешней модели, каждый со своей тестовой базой.
| Команда | Что проверяет |
|---|---|
npm run smoke | 92 детерминированные проверки через инструменты, без модели: пять тестов ТЗ и отрицательные случаи — подтверждение в том же ходу, без согласия, на другой товар, повторное; маскирование, поиск, вложения |
npm run demo | пять тестов ТЗ через агентный цикл: какие инструменты модель зовёт и в каком порядке. -- --live — на живой модели, -- --live --kk — по-казахски с шагом эскалации |
npm run mock-check | 22 свободные реплики в записанном режиме: русский и казахский, смена товара, согласие и отказ — страховка на случай без сети |
npm run check | валидация data/catalog.json |
npm run typecheck | TypeScript без сборки |
Архитектура
Схемы потока данных и подтверждения добавления.
| Слой | Решение |
|---|---|
| Приложение | Next.js 15 (App Router), React 19, TypeScript — один репозиторий |
| Агент | серверный цикл tool-calling в route handler, ответ стримится через SSE |
| Модель | OpenAI gpt-4.1-mini, запасная gpt-4.1-nano, записанный режим без сети |
| База | SQLite через @libsql/client: файл локально, Turso на Vercel |
| Проверка аргументов | zod |
| Хостинг | Vercel |
Браузер или виджет на сайте
│
├─ POST /api/chat ───────▶ агентный цикл ──▶ модель (tool-calling)
│ ◀─ SSE: текст, вызовы, │ ▲
│ состояние корзины ▼ │ результаты
│ 9 инструментов ──▶ SQLite / Turso
│
├─ POST /api/confirm ────▶ кнопка «Да» мимо модели ──▶ consumeProposal
├─ POST /api/upload ────▶ разбор файла / чтение фото ──▶ предложение
└─ GET /cart?c=токен ───▶ корзина только для чтения
Ключ к модели живёт только на сервере и в браузер не попадает.
| Слой | Файл | Роль |
|---|---|---|
| Конфигурация | src/lib/config.ts | модель, ключ, таймауты, лимиты — одно место |
| Доступ к модели | src/lib/llm/client.ts | стриминг, таймауты, ретраи, запасная модель, откат в mock |
| Чтение вложений | src/lib/llm/vision.ts | фото и PDF — отдельным вызовом, вне агентного цикла |
| Записанный режим | src/lib/llm/mock.ts | маршрутизация по намерению реплики, без сети |
| Агентный цикл | src/lib/agent/loop.ts | модель → инструменты → результаты → модель; язык ответа |
| Инструменты | src/lib/agent/tools.ts | девять тонких обёрток над БД + проверка согласия |
| Системный промпт | src/lib/agent/prompts.ts | правила поведения агента |
| Данные | src/lib/db.ts | схема, засев, поиск, аналоги, корзина, предложения |
| Разбор таблиц | src/lib/spec-parse.ts | xlsx, csv, docx — без внешних библиотек |
| Приватность | src/lib/redact.ts | маскирование платёжных и личных данных |
| Выгрузка каталога | scripts/seed-from-api.ts | однократный забор данных из API партнёра |
Инструменты
| Инструмент | Назначение |
|---|---|
search_catalog | поиск по каталогу, до 5 позиций |
get_product | карточка: характеристики, сертификат, цена, остатки по складам |
find_alternatives | аналоги, у каждого — поле reason с обоснованием |
find_related | сопутствующие товары из рекомендаций партнёра, с reason |
get_terms | оплата, доставка, самовывоз, гарантия, возврат, контакты менеджера |
propose_add | предложение добавить товар; корзину не меняет |
confirm_add | добавление по подтверждённому предложению |
get_cart | содержимое корзины |
get_cart_link | прямая ссылка на корзину |
Устойчивость
- Нет сети или ключа —
LLM_MODE=autoуводит в записанный режим, сценарий доходит до конца. В шапке загораетсяMOCK: выдавать записанный ответ за живую модель нельзя. - Rate limit, 5xx, таймаут — три попытки с экспоненциальной паузой, затем запасная модель, затем mock. Повтор запрещён, если часть ответа уже ушла пользователю.
- Мусор в аргументах — схема отклоняет, текст ошибки уходит модели, даётся одна попытка переписать вызов.
- Смена схемы БД — версия схемы хранится в таблице
metaи проверяется до применения схемы; при расхождении база пересобирается сама. Каталог засевается одной транзакцией: несколько экземпляров на Vercel стартуют одновременно и не видят его полузаполненным.
Правило подтверждения
Ключевое требование кейса: состав корзины не меняется без явного согласия клиента. Это не строчка в промпте, а инвариант на сервере.
- Модель не может добавить товар сама.
propose_addтолько записывает предложение со статусомpendingи возвращаетproposalId; корзина не трогается. - Подтверждение приходит одним из двух путей, и решение принимает
сервер, а не модель:
- кнопка «Да» —
POST /api/confirmидёт из интерфейса напрямую, минуя модель; подделать этот вызов модель не может, потому что в нём не участвует; - текст — модель вправе вызвать
confirm_add, но сервер исполнит его только после трёх проверок. Текст реплики клиента сервер сохраняет сам, до обращения к модели.
- кнопка «Да» —
- Три проверки текстового пути:
- предложение создано в предыдущем ходу — иначе модель в одном ответе предложила бы и сама подтвердила, подставив под проверку ту же фразу клиента;
- реплика — явное согласие. Вопросы («а можно добавить?»), условия («если есть на складе, добавь») и отрицания согласием не считаются; по-русски и по-казахски. Сомнительное — отказ;
- реплика не называет другой товар. «Добавь 027004» — новая просьба, а не согласие на висящее предложение про другой артикул. Эту дыру нашли на живом демо: модель попыталась подтвердить старое предложение на просьбу о новом товаре.
- Предложение одноразовое: условный
UPDATE … WHERE status='pending', гонкой двух запросов его не обойти. - Количество не больше доступного остатка. Если запрошено больше — добавляется доступное, и агент обязан об этом сказать.
- Спецификация из файла проходит через ту же функцию
consumeProposal. Второго пути изменения корзины в приложении нет.
Всё перечисленное покрыто npm run smoke, включая отрицательные случаи.
Данные
Каталог — боевая выгрузка из API партнёра, не выдумка:
GET https://ekt.kz/api/products?page=N список
GET https://ekt.kz/api/products/detail?id=N карточка с характеристиками и складами
scripts/seed-from-api.ts ходит туда один раз и пишет data/catalog.json,
который лежит в репозитории. В рантайме сеть к ekt.kz не нужна: приложение
читает SQLite. Демонстрация не зависит ни от чужого API, ни от Wi-Fi.
Срез: 300 позиций. Характеристики берутся из properties карточки
(номинальный ток, число полюсов, напряжение, отключающая способность),
сопутствующие товары — из properties.RECOMMEND, собственных
рекомендаций партнёра. Изначально мы читали это поле как список
аналогов; проверка на данных показала, что все связи ведут из
автоматов и УЗО на клеммы, то есть это допродажа, а не замена.
Аналоги подбираются по разделу каталога, типу, бренду и цене.
Что пришлось учесть в реальных данных
Не все склады торговые. В stores[] вперемешку городские склады и
служебные: «Брак MEGALIGHT», «Востановленный продукт», «перемещение»,
«Образцы Отдел Закупа», «Витрина ТЗ». Их остаток не считается доступным —
показать брак как наличие значит соврать клиенту. Поле quantity верхнего
уровня суммирует все склады, включая служебные, поэтому доступный остаток
пересчитывается по отфильтрованному списку.
Описание противоречит характеристикам. У товара id 515291 в названии
и описании «160А», а в properties.NOMINALNYY_TOK — «250 А». Источником
чисел объявлены только properties; описание показывается как текст и
никогда не служит источником характеристик. Это же зафиксировано
в системном промпте.
Грязные имена и артикулы. Названия встречаются с префиксом ***
и продублированным артикулом; поле article бывает служебным
(200300285_), а настоящий артикул поставщика лежит в
properties.ARTIKULPOSTAVSHCHIKA. Чистка делается на этапе выгрузки,
оригинал сохраняется в rawName.
Кириллица и латиница вперемешку. В каталоге есть и «16А», и «25A»
латиницей, и «3P», и артикул «404029х» с русской «х». Встроенный
LOWER() в SQLite понимает только латиницу, а LIKE для кириллицы
чувствителен к регистру: «узо» не находило «УЗО». Поэтому текст для
поиска готовится при засеве средствами JS: нижний регистр, ё → е,
латинские буквы-двойники — в кириллицу. Плюс синонимы (УЗО ↔ ВДТ,
автомат ↔ АВ, дифавтомат ↔ АВДТ) и бонус за совпадение с типом изделия:
«автомат 16А» ставит автоматы выше боксов для автоматов.
Что синтетическое
- Сертификаты. В API их нет, а требование кейса есть. Проставлены
примерно трети позиций, детерминированно по артикулу, с флагом
demo: true. Интерфейс и агент помечают такой сертификат как демонстрационный. Выдавать придуманный сертификат за настоящий нельзя.
Всё остальное — реальные данные партнёра. Артикулы, названия, цены,
характеристики, остатки и рекомендации — из API. Условия покупки
(data/terms.json) — с публичных страниц ekt.kz (/about/howto,
/about/faq, /about/contacts): оплата для физлиц и юрлиц, тарифы
доставки по Алматы, порог бесплатной доставки по стране, города
самовывоза, гарантия, возврат, контакты. Источник и дата сверки
записаны в самом файле.
Вложения
Раздел 5 ТЗ: вход — не только текст, но и файлы.
Спецификация (.xlsx, .xlsm, .csv, .tsv, .txt, .docx)
разбирается своим кодом, без внешних библиотек. Готовый пакет для xlsx
на npm тянет известные уязвимости, а разбирать предстоит файл, который
прислал кто угодно. Формат — zip с XML внутри, и нужная его часть
читается средствами Node: zlib умеет inflateRaw, остальное — разбор
заголовков. Из файла берутся только значения ячеек и текст: ни формулы,
ни макросы, ни внешние ссылки не исполняются.
PDF и фото накладной читает модель отдельным вызовом и возвращает строки «артикул — количество»; дальше они идут тем же путём, что и таблица. Фото модель сначала классифицирует: изделие или документ.
Артикулы сопоставляются с каталогом, найденное собирается в одно предложение на несколько позиций. Ненайденные строки показываются отдельным блоком — молча терять позиции из спецификации нельзя. Подтверждение при этом обычное: предложение проходит через ту же функцию, что и добавление одного товара.
Фото товара читается отдельным одноразовым вызовом модели, вне агентного цикла: картинка не попадает в поток сообщений и не трогает стриминг с ретраями. Сравнения изображений нет намеренно — в каталоге электротехника, где модульные автоматы внешне неотличимы, визуальное сходство почти не несёт сигнала. Зато на фото читается текст: артикул, бренд, номинал. Модель возвращает короткий JSON, дальше работает обычный поиск по каталогу.
Сопутствующие товары, казахский язык, менеджер
Сопутствующие. После подтверждения одной позиции сервер сам прикладывает до двух сопутствующих товаров из рекомендаций партнёра — и в ответ модели, и в ответ кнопки «Да». Агент предлагает их одной фразой с обоснованием, интерфейс — подсказками. В корзину они сами не попадают: это новое предложение и новое подтверждение.
Казахский. Агент отвечает на языке клиента. Язык определяет сервер, а не модель: правило в промпте живая модель игнорировала, отвечая на казахские реплики по-русски, — промпт и каталог на русском и перетягивают. Сервер смотрит на буквы, которых нет в русском алфавите (ә ғ қ ң ө ұ ү һ і), и добавляет на ход явную инструкцию. Названия и характеристики товаров не переводятся. Интерфейс переключается RU / KZ.
Правило подтверждения работает и на казахском: «иә», «жарайды», «қосыңыз» — согласие; «жоқ», «керек емес», «қоспа» — отказ. Вопрос с частицей «ма/ме» («қосуға бола ма») согласием не считается, даже без знака вопроса.
Менеджер. Крупная партия, претензия или возврат, доставка не по Алматы, прямая просьба позвать человека — агент даёт реальные контакты из условий покупки и не обещает обратного звонка. В интерфейсе есть кнопка «Связаться с менеджером».
Безопасность и приватность
ТЗ требует учесть приватность клиента и защиту от несанкционированного изменения корзины. Что сделано:
- Ссылка на корзину — только для чтения. В ней случайный токен, а не
идентификатор сессии: сессия даёт право менять корзину через чат,
а ссылкой клиент делится. По токену сессию не восстановить.
Идентификатор сессии создаётся через
crypto.randomUUID(). - Платёжные данные не хранятся. Номер карты (с проверкой Луна), CVV и IBAN вырезаются из реплики до всего остального — в модель, журнал сервера и историю, которую видит модель, попадает метка. Лента чата сохраняется в браузер клиента тоже уже с меткой. Агент говорит клиенту, что в чате платёжные данные не нужны. В журнал дополнительно не пишутся телефон, email и ИИН.
- Журнал сервера наружу не отдаётся. Он общий для всех сессий.
- Лимиты: 20 сообщений и 6 файлов в минуту с адреса, реплика до 2000 символов. Лимитер в памяти экземпляра — защита от одного назойливого клиента, не от распределённой атаки; для прода нужен WAF.
- Встраивание во фрейм разрешено только нам и ekt.kz
(
frame-ancestors): иначе чужая страница может накрыть кнопку «Да» прозрачным фреймом. - Вложения не исполняются: из xlsx и docx берутся только значения ячеек и текст, без формул, макросов и внешних ссылок.
База на Vercel
На serverless у каждой функции свой /tmp. С файловой SQLite чат,
подтверждение и страница корзины попадали в разные базы, и страница
корзины оказывалась пустой — это проверено на проде. Поэтому публичная
версия работает на Turso (удалённый libSQL): переменные подставляет
интеграция Turso в Vercel, код тот же. С ноутбука процесс один,
и файловой базы достаточно.
Встраивание в сайт
Одна строка на любой странице, независимо от платформы сайта:
<script src="https://hackalem10.vercel.app/embed.js" async></script>
Скрипт рисует кнопку и панель с iframe на /embed. Стили изолированы
дважды — shadow DOM и самим iframe, — поэтому ни сайт не ломает чат, ни
чат сайт. Связь с родительской страницей идёт через postMessage с
проверкой источника и origin с обеих сторон; по каналу ходят только
служебные сообщения — готовность, высота, закрытие. Данные корзины
и диалога через него не передаются.
/demo-site— сторонняя витрина с другим оформлением и подключённым виджетом./try— закладка для браузера: откройте ekt.kz, нажмите закладку — виджет появится на живом сайте партнёра. Сам сайт при этом не меняется: виджет видит только тот, кто нажал закладку.
Ограничения
По требованиям кейса
- агент не оформляет заказ — последний шаг это ссылка на корзину;
- не запрашивает и не хранит платёжные данные;
- не меняет корзину без явного подтверждения;
- не сообщает недостоверных данных о цене и наличии: все числа приходят из инструментов, модель их не придумывает и не пересчитывает;
- если данных нет — говорит, что не нашёл, и предлагает связаться с менеджером. Выдумывать артикулы запрещено.
Ограничения прототипа
- Каталог — срез из 300 позиций и снимок на момент выгрузки. Цены и остатки не обновляются в реальном времени; в проде — периодическая синхронизация через тот же API.
- Корзина своя, а не корзина Bitrix. Для настоящей интеграции нужен API корзины сайта; сейчас ссылка ведёт на нашу страницу корзины.
- Сертификаты синтетические и помечены как демонстрационные.
- История диалога — в браузере, в пределах сессии. Для авторизованных клиентов id должен приходить с сервера Bitrix с подписью: верить идентификатору из браузера нельзя.
- Лимитер запросов — в памяти экземпляра. Для прода — WAF или лимитер на Redis.
Как делали: Claude и Codex
Условие хакатона — использовать Codex. Роли разделены в AGENTS.md: архитектура, контракты инструментов, правила поведения агента, приёмочные критерии и системный промпт — одна зона; реализация кода — Codex.
Каждая задача Codex — отдельная постановка в docs/codex/
(01–08): какие файлы менять, что трогать нельзя, чем проверить результат.
Каждый результат проверялся прогоном, а не по отчёту. Грабли, на которые
наступили за день, собраны в конце AGENTS.md: от \b, который
не видит кириллицу, до базы, которая на Vercel у каждой функции своя.
- docs/codex/HANDOVER.md — состояние проекта, инварианты и безопасная процедура деплоя;
- docs/DEFENSE.md — ответы на вопросы жюри.
Структура репозитория
src/app/ страницы и API: /, /cart, /embed, /demo-site, /try,
/api/chat, /api/confirm, /api/upload, /api/state, /api/reset, /api/products
src/components/ витрина, чат, корзина, карточка подтверждения, i18n
src/lib/agent/ агентный цикл, инструменты, системный промпт
src/lib/llm/ доступ к модели, записанный режим, чтение фото и PDF
src/lib/db.ts схема, засев, поиск, аналоги, корзина, предложения
src/lib/redact.ts маскирование платёжных и личных данных
src/lib/ratelimit.ts лимиты запросов
src/lib/spec-parse.ts разбор xlsx, csv, docx
public/embed.js виджет одной строкой
data/ каталог и условия покупки
scripts/ выгрузка каталога, smoke, demo, mock-check, образцы вложений
docs/ постановки для Codex, передача, ответы жюри
AGENTS.md спецификация и грабли
Авторы коммитов
История коммитов
- 2026-09-23 17:43На мобильных показывать чат сразу под корзинойiwbhappy
- 2026-09-23 17:37README: клонирование репозитория и версия Node.js в разделе «Запуск»iwbhappy
- 2026-09-23 17:35README и DEFENSE: пробел с лентой в браузере закрыт в 97cbecciwbhappy
- 2026-09-23 17:34Лента чата в браузере тоже не хранит платёжные данныеiwbhappy
- 2026-09-23 17:30README: точная формулировка про платёжные данныеiwbhappy
- 2026-09-23 17:26Сохранить авторскую редакцию README кроме ссылки на архитектуруiwbhappy
- 2026-09-23 17:21Актуализировать тексты интерфейса и шпаргалку для защитыiwbhappy
- 2026-09-23 17:20Документировать потоки и серверное подтверждение схемами Mermaidiwbhappy
Все коммиты (52)
- 2026-09-23 17:17README к сдаче: порядок для жюри, актуальные факты, честные ограниченияiwbhappy
- 2026-09-23 17:17Уточнить ошибки интерфейса и перенос длинных названийiwbhappy
- 2026-09-23 17:15Добавить единую команду verify перед сдачейiwbhappy
- 2026-09-23 17:14README: сопоставить требования ТЗ с реализацией и проверкамиiwbhappy
- 2026-09-23 17:13README: добавить проверку за пять минут для жюриiwbhappy
- 2026-09-23 17:09HANDOVER: прод работает с Turso, процедура чистого деплояiwbhappy
- 2026-09-23 17:06Поиск находит кириллицу: «УЗО», «16А», «Автоматический» больше не теряютсяiwbhappy
- 2026-09-23 17:04Добавить страницу с закладкой для демонстрации виджетаiwbhappy
- 2026-09-23 17:01Показать сопутствующие товары после подтверждения кнопкойiwbhappy
- 2026-09-23 17:00База готова к Turso: каталог одной транзакцией, версия схемы в таблицеiwbhappy
- 2026-09-23 17:00Исправить маршрутизацию офлайн-режима и добавить mock-checkiwbhappy
- 2026-09-23 16:51Постановка 08: шлифовка к сдаче — README для жюри, соответствие ТЗ, verify, схемыiwbhappy
- 2026-09-23 16:48Постановка 07: записанный режим, который выдерживает вопросы жюриiwbhappy
- 2026-09-23 16:47Кнопка «Да» возвращает сопутствующие товары, как и confirm_add в чатеiwbhappy
- 2026-09-23 16:36Согласие на другой товар не подтверждает висящее предложениеiwbhappy
- 2026-09-23 16:25Добавить историю и безопасную сессию в интерфейсiwbhappy
- 2026-09-23 16:22Смоук: проверка маскирования телефона стала настоящейiwbhappy
- 2026-09-23 16:22AGENTS §13: исправлена заметка, испорченная той граблей, которую она описываетiwbhappy
- 2026-09-23 16:22Документация: безопасность, вложения, разделённая база на Verceliwbhappy
- 2026-09-23 16:21Образцы вложений — бинарные: git не трогает переводы строк в PDFiwbhappy
- 2026-09-23 16:21Вложения по ТЗ §5 полностью: Word, PDF, фото накладнойiwbhappy
- 2026-09-23 16:17Приватность и защита публичной ссылки: маскирование, лимиты, заголовкиiwbhappy
- 2026-09-23 16:15Ссылка на корзину только для чтения; журнал не уходит наружуiwbhappy
- 2026-09-23 16:14Постановка 06: криптостойкая сессия, история диалога, Word и PDFiwbhappy
- 2026-09-23 16:06Добавить казахский интерфейс и связь с менеджеромiwbhappy
- 2026-09-23 16:03Сопутствующие товары; RECOMMEND партнёра больше не выдаётся за аналогiwbhappy
- 2026-09-23 15:52Документация: реальные условия, казахский, виджет, три новые граблиiwbhappy
- 2026-09-23 15:51Язык ответа и ссылку на корзину решает сервер, а не модельiwbhappy
- 2026-09-23 15:48Постановка 05: казахский интерфейс и кнопка менеджераiwbhappy
- 2026-09-23 15:47Условия покупки с ekt.kz, казахский язык, эскалация на менеджераiwbhappy
- 2026-09-23 15:45Виджет для встраивания: embed.js, /embed и демо-витрина (Codex)iwbhappy
- 2026-09-23 15:22Вложения закрыты: отмечаю в передачеiwbhappy
- 2026-09-23 15:21Передача проекта: состояние, инварианты и защитаiwbhappy
- 2026-09-23 15:15Разбор спецификаций без внешних зависимостейiwbhappy
- 2026-09-23 15:12Вложения: батч-предложения и чтение фото товараiwbhappy
- 2026-09-23 15:01Постановка 03: id каталога, чистка хвостовых маркеров, проверка демоiwbhappy
- 2026-09-23 15:00Интерфейс: витрина, чат и постоянно видимая корзина (Codex)iwbhappy
- 2026-09-23 15:00Промпт: добавление в корзину без лишнего шагаiwbhappy
- 2026-09-23 14:44Аналоги называют расхождения характеристик; живой прогон на моделиiwbhappy
- 2026-09-23 14:34Питч под кейс ekt.kz и раздел с граблямиiwbhappy
- 2026-09-23 14:32Серверная часть кейса ekt.kz: инструменты, данные, приёмочные тестыiwbhappy
- 2026-09-23 14:00WIP: кейс ekt.kz — инструменты, БД и подтверждение покупкиiwbhappy
- 2026-09-23 13:49spec: кейс ekt.kz, выгрузка из API партнёра, серверное подтверждениеiwbhappy
- 2026-09-23 06:58Каркас агентного торгового приложенияiwbhappy
Структура репозитория (73 файлов)
.claude/launch.json .env.example .gitattributes .gitignore AGENTS.md HackAlem AI_ ИИ-ассистент для чата на сайте ekt.kz.docx README.md data/catalog.json data/products.json data/terms.json docs/ARCHITECTURE.md docs/CODEX.md docs/DEFENSE.md docs/PITCH.md docs/TIMELINE.md docs/codex/01-seed-from-api.md docs/codex/02-ui.md docs/codex/03-catalog-id.md docs/codex/04-attachments.md docs/codex/05-kazakh-ui.md docs/codex/06-session-history.md docs/codex/07-offline-mock.md docs/codex/08-polish.md docs/codex/HANDOVER.md next.config.ts package-lock.json package.json public/embed.js scripts/check-catalog.ts scripts/demo.ts scripts/fixtures/invoice-photo.jpg scripts/fixtures/invoice.pdf scripts/fixtures/spec-lines.docx scripts/fixtures/spec-table.docx scripts/mock-check.ts scripts/seed-from-api.ts scripts/smoke.ts src/app/api/chat/route.ts src/app/api/confirm/route.ts src/app/api/products/route.ts src/app/api/reset/route.ts src/app/api/state/route.ts src/app/api/upload/route.ts src/app/cart/page.tsx src/app/demo-site/page.tsx src/app/embed/page.tsx src/app/globals.css src/app/layout.tsx src/app/page.tsx src/app/try/page.module.css src/app/try/page.tsx src/components/AgentApp.tsx src/components/CartPanel.tsx src/components/ConfirmCard.tsx src/components/EmbedBridge.tsx src/components/EmbedDemoSite.tsx src/components/ProductGrid.tsx src/components/i18n.ts src/lib/agent/bridge.ts src/lib/agent/loop.ts src/lib/agent/prompts.ts src/lib/agent/tools.ts src/lib/config.ts src/lib/db.ts src/lib/llm/client.ts src/lib/llm/mock.ts src/lib/llm/types.ts src/lib/llm/vision.ts src/lib/logger.ts src/lib/ratelimit.ts src/lib/redact.ts src/lib/spec-parse.ts tsconfig.json
Ветки (2)
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.