AAYMA
RU
Войти

ИИ-консультант ekt.kz

Чат-ассистент по реальному каталогу ekt.kz на русском и казахском: поиск товара, остатки по складам, цены, аналоги.

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

О проекте

Команда: JIGER
Кейс HackAlem.ai: Торговля — Электрокомплект · ekt.kz
Стадия: Рабочий MVP

Задача кейса: AI-консультант по каталогу ekt.kz: товары, аналоги, цены, остатки, корзина

Суть

Чат-ассистент по реальному каталогу ekt.kz на русском и казахском: поиск товара, остатки по складам, цены, аналоги. Товар добавляется в корзину в два шага с проверкой остатка. Ассистент читает вложения (фото, PDF, Excel, Word).

Как устроено

fetch_catalog.py выгружает каталог из API ekt.kz в JSON-кэш, в памяти строятся индексы id и артикула. Агент на OpenAI Chat Completions (gpt-4o-mini) с function calling вызывает search_products, get_product, find_analogs, list_categories, propose_cart_add и confirm_cart_add. Аналоги подбираются по категории и сходству описаний, до 5 кандидатов. Перед подтверждением корзины остаток сверяется с live-API ekt.kz. Условия покупки берутся из data/terms.md как единственный источник. Вложения разбираются через pypdf, openpyxl, xlrd и python-docx, изображения идут в vision-модель.

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

  • function calling
  • two-step cart confirmation
  • analog search by category+description similarity
  • attachment parsing
  • live stock check
  • secret redaction
  • prompt-injection guard

Особенности

Статус каждого требования кейса честно размечен в матрице. Собран набор из 15 тестовых вложений, включая битые файлы и «подтверждение внутри файла» как защиту от инъекции. Перед добавлением в корзину остаток проверяется по live-API с фолбэком на кэш. Казахские слова подтверждения («иә», «жарайды») распознаются.

Стек

Python · FastAPI · Uvicorn · httpx · pypdf · openpyxl · python-docx · pytest · respx · uv · Docker Compose · HTML/JS

Модели

gpt-4o-mini


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

README

README.md

ИИ‑консультант для интернет‑магазина ekt.kz

Команда JIGER · хакатон HACKALEM AI · кейс партнёра ТОО «Электрокомплект» (ekt.kz): «ИИ‑ассистент‑консультант и помощник для сайта ekt.kz».

Чат‑ассистент, который отвечает покупателю на вопросы о товарах ekt.kz по реальным данным каталога (наличие по складам, цена, характеристики, ссылка на карточку), подбирает аналоги, если позиции нет, и добавляет товар в корзину только после явного подтверждения покупателя с проверкой остатка. Работает на русском и казахском языках.

Развёрнутой версии нет. Инструкции для локального запуска и Docker находятся в разделе 4.


1. Что делает проект

Проблема (из кейса). Чтобы узнать наличие, характеристики или аналог товара, клиент ekt.kz пишет или звонит менеджеру. Решение о покупке затягивается, менеджеры тратят время на однотипные вопросы.

Решение. Единое окно чата на сайте, где ассистент:

  • находит товар по артикулу, названию, бренду, характеристикам, цене («до 3000 тенге») и разделу каталога;
  • отвечает, есть ли товар в наличии и на каких складах сколько штук;
  • показывает цену, характеристики, минимальную партию и ссылку на страницу товара на ekt.kz;
  • если товара нет — предлагает аналоги из каталога и объясняет, чем они отличаются;
  • по просьбе покупателя добавляет товар в корзину строго в два шага: предложение → явное «да», с проверкой остатка и кратности на сервере, и даёт прямую ссылку на корзину;
  • понимает вопросы на русском и казахском;
  • принимает фото товара, спецификации и накладные в JPEG, PNG, PDF, Excel и Word;
  • отвечает только по данным каталога и не выдумывает цены и наличие;
  • отказывается отвечать на вопросы не о товарах.

Ответ требует доступа к OpenAI. Время зависит от модели, размера вложений, числа вызовов инструментов и сети; воспроизводимого замера задержки в репозитории нет.

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

Рабочая матрица требований кейса

Официальный текст ТЗ и опубликованная методика допуска в репозитории отсутствуют. Нумерация ниже — рабочая, согласованная с docs/test-cases.xlsx; её необходимо сверить с документами организатора. Статусы описывают реализацию прототипа и не подтверждают официальный допуск.

№Рабочее требованиеСтатусКак реализовано / как проверить
1Поиск, наличие, характеристики и сертификаты по данным каталогаЧастичноsearch_products и get_product возвращают сведения из загруженного каталога. Ссылки на сертификаты передаются, если присутствуют в данных; в текущем реальном снимке их нет. Проверки на синтетических ссылках не подтверждают наличие реальных документов. Проверка товара — артикул 010400570_, раздел 5.
2Аналоги при отсутствии товара с обоснованиемРеализовано с ограничениямиfind_analogs (app/analogs.py) выбирает до 5 доступных кандидатов по категории и сходству описаний. Совпадение категории не доказывает электрическую или механическую совместимость: характеристики нужно сравнивать отдельно. Проверка — артикул 310100080_ (остаток 0), раздел 5.
3Условия покупки: оплата, доставка, минимальная партияДаУсловия и контакты лежат в data/terms.md (собраны с публичных страниц ekt.kz «Доставка и оплата», «Как сделать заказ», «Условия возврата и обмена», «Контакты» 23.09.2026; партнёру достаточно править этот файл, он перечитывается без перезапуска). Блок попадает в системный промпт как единственный источник: чего в нём нет, ассистент не выдумывает, а направляет к менеджеру (app/terms.py, правила 11–13 в app/prompts.py). Минимальная партия по товару (KRATNOST_MIN) выдаётся и учитывается при добавлении в корзину.
4Добавление в корзину только после явного подтвержденияРеализовано для прототипаpropose_cart_add готовит предложение без изменения корзины; несколько разных товаров в одном ходе объединяются в список. confirm_cart_add проверяет явное согласие в следующем сообщении, остатки всех позиций и добавляет весь список либо ничего. Отказ, оговорка или изменение количества требуют нового предложения. Корзина привязана к HttpOnly-cookie ekt_session. Проверка — раздел 5, сценарии 4–6 и накладная из примеров вложений.
5Контроль количества и ссылка на корзинуРеализовано для прототипаКоличество проверяется по остатку и кратности. После подтверждения выдаётся {PUBLIC_BASE_URL}/cart; GET /cart и GET /api/cart показывают состав текущей cookie-сессии. Интеграции с корзиной ekt.kz, оплаты и оформления настоящего заказа нет.

Вложения

POST /chat принимает до 3 файлов по 10 МБ каждый: .jpg, .jpeg, .png, .pdf, .xlsx, .xls, .docx. Проверяются расширение и MIME-тип. Можно отправить файл без текста. JPEG и PNG передаются модели как изображения; PDF читается через pypdf (первые 10 страниц), XLSX — openpyxl, XLS — xlrd, DOCX — python-docx (абзацы и таблицы). Из каждого документа извлекается до 8000 символов вместе с пометкой об обрезке. Лимит 1000 символов относится только к набранному сообщению.

Для спецификаций ассистент должен выделить артикулы, названия и количества, проверить каждую позицию через search_products, показать наличие и нехватку, затем предложить добавление с отдельным подтверждением покупателя. По фото он описывает видимые признаки и ищет по названию или маркировке. Текст внутри вложения не считается согласием на добавление.

Готовые документы, изображения, сценарии ошибок и ожидаемые результаты: инструкция по проверке вложений, архив файлов. PDF без текстового слоя не распознаётся: для такого документа приложите JPG/PNG либо PDF с текстом.

Опциональные требования

ТребованиеСтатусКомментарий
Казахский языкПоддержка реализована; качество проверяется с модельюОпределение языка + правило промпта: ответ на языке вопроса, поиск по русскому каталогу; слова подтверждения корзины («иә», «жарайды», «қос») распознаются. Ошибки подбора всё ещё возможны.
Рекомендации сопутствующих товаровЧастичноАссистент предлагает альтернативы и «показать ещё» по фильтрам; отдельного механизма «сопутствующих» нет.
История диалога для авторизованныхНетИстория хранится в браузере в рамках открытой вкладки (последние 10 реплик передаются с каждым запросом); корзина живёт в cookie‑сессии до 7 дней бездействия.
Эскалация на менеджераДаПравило промпта: крупный опт, скидки и спецусловия, тендеры, поставка под заказ, рекламации, договор или вопрос вне каталога и условий — ассистент говорит, что это решает менеджер, и даёт контакты из data/terms.md (телефоны, WhatsApp, e-mail и адреса филиалов, портал B2B), ничего не выдумывая.

Что сделано сверх матрицы

  • Карточки товаров в чате: фото, цена, бейдж «В наличии · N шт.» / «Нет в наличии», артикул, ссылка на ekt.kz; кнопка «Корзина» в шапке (static/chat.html).
  • Фильтры и пагинация поиска: только в наличии, диапазон цен, раздел каталога, «покажи ещё» (offset, total); инструмент list_categories предлагает реальные разделы, если запрошенной категории нет.
  • Контекст диалога: id показанных товаров и состав корзины передаются модели, поэтому уточнения вроде «а в каких городах есть второй?» или «что у меня в корзине?» не требуют повторного поиска.
  • Загрузчик каталога fetch_catalog.py: постранично забирает список товаров и полную карточку каждого из API ekt.kz с повторами при сбоях и атомарной записью кэша.
  • Безопасность: настроенные секреты маскируются в логах и ответах (redact_secrets); промпт запрещает исполнять инструкции из пользовательских данных; фронтенд и корзина экранируют HTML и проверяют схемы ссылок. Идентификатор сессии выдаёт сервер (HttpOnly, SameSite=Lax); неизвестная cookie заменяется новой пустой сессией. Ограничены сообщение, история, вложения и число раундов инструментов. Промпт запрещает запрашивать платёжные реквизиты; это не фильтр персональных данных, отправленных пользователем самостоятельно.
  • Приватность логов: по умолчанию в лог попадают только длина сообщения, id ответа OpenAI и имена инструментов; текст сообщений покупателя, аргументы инструментов и ответы пишутся только при LOG_LEVEL=DEBUG.
  • Скорость и актуальность: каталог читается с диска один раз при старте и кэшируется в памяти с индексами id → товар и артикул → товар (точный поиск по артикулу за O(1)); файл перечитывается при замене (reload_catalog()). Перед подтверждением добавления в корзину остаток товара сверяется с API ekt.kz (таймаут 5 с, при заданных EKT_API_USER/EKT_API_PASSWORD); если API недоступен, используется кэш и ассистент говорит об этом покупателю.
  • Устойчивость: понятные сообщения при таймаутах, лимитах и ошибках OpenAI; если ответ с предложением не дошёл до покупателя, предложение снимается.
  • Docker: запуск через Compose, отдельный непривилегированный пользователь, healthcheck.
  • Тесты и тест‑кейсы: офлайн‑тесты инструментов, корзины и вложений с подменой внешних сервисов; отдельно запускаемые live‑тесты API ekt.kz; ручные сценарии по рабочей матрице (docs/test-cases.xlsx). Автотесты с подменой OpenAI не оценивают качество ответов настоящей модели.

3. Используемые технологии и архитектура

Стек

СлойТехнология
ЯзыкPython 3.11+
Веб‑серверFastAPI + Uvicorn
LLMOpenAI Chat Completions API с function calling (модель из MODEL, по умолчанию gpt-4o-mini)
HTTP‑клиентhttpx (загрузка каталога ekt.kz)
Вложенияpython-multipart, pypdf, openpyxl, xlrd, python-docx; изображения — vision выбранной модели
ДанныеJSON‑кэш каталога data/products.json; корзины — in‑memory на сервере (без БД)
ФронтендОдин HTML‑файл: чистый HTML/CSS/JS без сборки и фреймворков; страница корзины рендерится сервером
Зависимостиuv (pyproject.toml, uv.lock) + экспортированный requirements.txt
Тестыpytest, pytest‑asyncio, respx
КонтейнеризацияDocker, docker compose

Схема

 Браузер покупателя                     Сервер (FastAPI)                            Внешние сервисы
 ─────────────────                     ────────────────                            ───────────────
 static/chat.html ── POST /chat ─────▶ app/main.py ──── cookie ekt_session ──▶ app/cart.py (in-memory корзины)
   message, history[-10],                  │                                          ▲
   product_ids, files ──────────────▶ app/attachments.py                               │
                                           ▼                                          │
                                       app/agent.py  (цикл агента, ≤5 раундов)        │
                                           │ system prompt (app/prompts.py)           │
                                           │ + показанные товары + состав корзины     │
                                           ├──── chat.completions ───────────────────────────▶ OpenAI API
                                           │◀─── tool_calls ────────────────────────────────
                                           ├─ search_products / list_categories / get_product / find_analogs
                                           │        └──▶ app/catalog.py ──▶ data/products.json ◀── fetch_catalog.py ◀── https://ekt.kz/api/products
                                           ├─ propose_cart_add / confirm_cart_add / cancel_cart_proposal / get_cart ─┘
                                           ▼
 карточки товаров ◀── {reply, products} ───┘
 страница корзины ◀── GET /cart, GET /api/cart ── app/cart_page.py

Поток одного запроса

  1. Браузер отправляет POST /chat как FormData: текст вопроса, историю, id показанных товаров и вложения. Cookie ekt_session идентифицирует корзину (выдаётся сервером при первом запросе). JSON-запросы без файлов также поддерживаются.
  2. Сервер проверяет файлы, извлекает текст документов и формирует блоки «Содержимое вложения «имя файла»: …». Изображения кодируются в base64 data URL и передаются в image_url-частях сообщения. Вложения доступны модели в запросе с загрузкой; браузер сохраняет в истории набранный текст, имена файлов и ответ, но не содержимое файлов. Для нового анализа документа его нужно приложить повторно.
  3. Сервер собирает системный промпт: правила консультанта, условия покупки и контакты из data/terms.md, данные уже показанных товаров, текущий состав корзины и ожидающее подтверждения предложение, подсказка языка.
  4. Модель вызывает инструменты каталога (search_products с фильтрами, get_product, list_categories, find_analogs) и корзины. Несколько вызовов инструментов могут быть запрошены в одном раунде.
  5. Результаты инструментов возвращаются модели; после не более чем 5 раундов модель формирует текстовый ответ.
  6. Сервер отбирает товары, упомянутые в ответе, и отдаёт {"reply": "...", "products": [...]}.
  7. Фронтенд экранирует значения при рендеринге Markdown‑ответа и показывает карточки товаров с фото, ценой и остатком.

Инструменты модели (function calling)

ИнструментАргументыЧто делает
search_productsquery, max_results (1–20), in_stock_only, min_price, max_price, category, offsetПоиск по артикулу и ключевым словам с фильтрами; возвращает total, offset, items (id, название, артикул, цена, категория, ссылка, фото, остаток, in_stock, склады с остатком > 0). Точный артикул → первым, затем товары в наличии, затем по релевантности и цене.
list_categories—Разделы каталога (два уровня из URL товара) с числом товаров и числом в наличии.
get_productproduct_idПолная карточка: описание, характеристики (properties), минимальная партия (min_batch), остатки по складам и certificate_urls из исходных данных.
find_analogsskuДо 5 товаров той же категории с остатком > 0, отсортированных по сходству описаний, с полем reason.
propose_cart_addproduct_id, quantityШаг 1: проверяет товар, остаток, уже лежащее в корзине количество и кратность. Вызовы для разных товаров в одном ходе собирают общее предложение; повтор того же id заменяет его количество. При нарушении возвращает error с max_quantity / suggested_quantity. Корзина не меняется.
confirm_cart_add—Шаг 2: после явного согласия без оговорок в следующем сообщении перепроверяет все позиции и атомарно переносит предложение в корзину либо возвращает ошибку без добавления. Возвращает результат и cart_url.
cancel_cart_proposal—Снимает неподтверждённое предложение.
get_cart—Текущий состав корзины, суммы и ссылка.

HTTP‑интерфейс

Метод и путьНазначение
GET /Интерфейс чата
POST /chatMultipart: текстовое message, поля history и product_ids как JSON-строки, повторяющееся поле files; либо прежний JSON {message, history, product_ids}. Ответ: {reply, products}. Выдаёт cookie сессии
GET /cartHTML‑страница корзины текущей сессии
GET /api/cartСостав корзины в JSON
GET /docsSwagger‑документация

Состав репозитория

ПутьНазначение
app/main.pyFastAPI‑приложение: маршруты, cookie‑сессия, валидация и обработка ошибок
app/attachments.pyЛимиты multipart, проверка вложений, извлечение текста и формирование частей сообщения для vision
app/agent.pyЦикл агента: промпт, вызовы OpenAI, описание и выполнение инструментов (в том числе корзины), логирование без секретов
app/catalog.pyКэш каталога в памяти с индексами по id и артикулу, поиск с фильтрами, разделы, карточки, остаток и кратность
app/analogs.pyДетерминированный подбор аналогов по категории и сходству описаний
app/stock.pyСвежий остаток товара из API ekt.kz перед подтверждением добавления в корзину
app/terms.pyУсловия покупки и контакты из data/terms.md для системного промпта
data/terms.mdУсловия оплаты, доставки, возврата и контакты менеджеров (снимок публичных страниц ekt.kz)
app/cart.pyКорзины покупателей: потокобезопасное in‑memory хранилище по cookie‑сессии, серверная проверка явного подтверждения
app/cart_page.pyСерверный рендер страницы /cart с экранированием значений
app/prompts.pyСистемный промпт консультанта (правила честности, языка, формата, корзины)
app/__init__.pyredact_secrets — маскирование секретов в логах и ответах
static/chat.htmlИнтерфейс чата (десктоп и мобильный)
fetch_catalog.pyЗагрузка каталога из API ekt.kz в data/products.json
data/products.jsonКэш каталога: 400 товаров (входит в репозиторий)
tests/Фикстуры (снимки API, синтетический каталог, условия) и тесты
docs/test-cases.xlsxРучные тест‑кейсы по рабочей матрице 1–5 (генератор docs/make_test_cases.py); статусы не являются результатом автотестов
docs/attachment-samples/Файлы, инструкция и ожидаемые результаты проверки вложений
Dockerfile, docker-compose.ymlКонтейнерный запуск

4. Порядок установки и запуска

Требования

  • Python 3.11 или новее (либо Docker);
  • ключ OpenAI API;
  • доступ к API ekt.kz необязателен для запуска: готовый кэш data/products.json уже лежит в репозитории; авторизация нужна для обновления каталога и проверки свежих остатков.

Переменные окружения (.env)

cp .env.example .env
ПеременнаяОбязательнаНазначение
OPENAI_API_KEYдаключ OpenAI
MODELнетмодель OpenAI, по умолчанию gpt-4o-mini; для фото нужна поддержка изображений, для работы агента — вызовов инструментов
PUBLIC_BASE_URLнетадрес, по которому покупатель открывает сайт; из него собирается ссылка на корзину {PUBLIC_BASE_URL}/cart, по умолчанию http://localhost:8000 (при https:// cookie сессии выдаётся с флагом Secure)
EKT_API_USER, EKT_API_PASSWORDнетBasic Auth к https://ekt.kz/api/products: нужны для fetch_catalog.py и для проверки свежего остатка перед добавлением в корзину (без них остаток берётся из кэша)
LOG_LEVELнетDEBUG — писать в лог текст сообщений, аргументы инструментов и ответы; иначе только длины, id ответа OpenAI и имена инструментов
ALLOWED_ORIGINSнетисточники для CORS через запятую; по умолчанию * (демо, без cookie между доменами). Для сайта партнёра: ALLOWED_ORIGINS=https://ekt.kz,https://www.ekt.kz — тогда включается allow_credentials, и cookie корзины принимается с этих доменов

Вариант A — uv (рекомендуется)

git clone [email protected]:BAITC-Hacks/hack-c471837d-jiger.git
cd hack-c471837d-jiger
cp .env.example .env            # вписать OPENAI_API_KEY
uv sync --locked
uv run python -m uvicorn app.main:app --host 0.0.0.0 --port 8000

Вариант B — venv + pip

python -m venv .venv
source .venv/bin/activate       # Windows PowerShell: .\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000

Вариант C — Docker

cp .env.example .env            # вписать OPENAI_API_KEY
docker compose up --build

Папка data/ монтируется в контейнер, healthcheck опрашивает /docs.

После запуска откройте http://localhost:8000 — чат; http://localhost:8000/cart — корзина; http://localhost:8000/docs — Swagger.

Обновление каталога (по желанию)

uv run python fetch_catalog.py --max-pages 20    # нужны EKT_API_USER / EKT_API_PASSWORD в .env

Скрипт постранично забирает список товаров и полную карточку каждого, повторяет запросы при сбоях и атомарно заменяет data/products.json. Без кэша приложение не стартует и печатает подсказку запустить этот скрипт.

Тесты

# офлайн-тесты (без OpenAI и без сети)
uv sync --locked
uv run pytest

# live-тесты API ekt.kz (отдельно задайте EKT_USER и EKT_PASSWORD в .env)
uv run pytest -o "addopts=" -m live tests/test_ekt_api.py

Что покрыто офлайн (синтетический каталог tests/fixtures/catalog_small.json, HTTP к ekt.kz подменён через respx, модель OpenAI подменена сценарием вызовов инструментов):

  • tests/test_tools_product.py — поиск по существующему артикулу, «не найдено» для несуществующего, ссылки на сертификаты из описания;
  • tests/test_tools_analogs.py — минимум один аналог в наличии для каждого товара с нулевым остатком;
  • tests/test_cart.py — cookie сессии, корзина не меняется без явного «да», отказ и отмена, ограничение остатком с учётом уже добавленного, изоляция сессий, устаревшее предложение, кратность, перепроверка остатка перед подтверждением;
  • tests/test_catalog_stock.py — остатки и склады из каталога: только склады с остатком, фильтр «в наличии», понятная ошибка при отсутствующем или битом кэше;
  • tests/test_security.py — секреты не попадают в логи, экранирование и безопасные ссылки на странице корзины, границы ввода /chat, история как данные, карточки только для упомянутых товаров;
  • tests/test_stock.py — свежий остаток из API: Basic Auth уходит только на https://ekt.kz, любая ошибка API откатывает проверку на кэш, чужой или неполный ответ не принимается;
  • tests/test_terms.py — условия покупки: файл доходит до системного промпта, правка подхватывается без перезапуска, без файла ассистент обязан отказаться, а не выдумать;
  • tests/test_limits.py — ограничения ТЗ: приватный лог по умолчанию и полный при LOG_LEVEL=DEBUG, кэш каталога и индекс по артикулу, свежий остаток из API перед подтверждением (сеть подменена respx), ALLOWED_ORIGINS, условия покупки из data/terms.md.
  • tests/test_attachments.py — извлечение документов, части сообщения с изображениями, лимиты, неподдерживаемые типы и совместимость JSON/multipart. OpenAI в этих проверках подменяется.

Группа dev в pyproject.toml фиксирует тестовые зависимости через uv.lock; обычный uv sync --locked устанавливает её. requirements.txt экспортируется без группы dev и содержит зависимости приложения для pip/Docker. Число пройденных тестов смотрите в результате текущего запуска. Проверки API ekt.kz по умолчанию исключены маркером live; они используют сеть и требуют отдельных учётных данных. Для live-тестов обязателен EKT_USER; пароль берётся из EKT_PASSWORD, либо из EKT_API_PASSWORD. Один лишь EKT_API_USER не включает эти проверки. Проверки с подменённой моделью подтверждают обработку данных и бизнес-логику, но не точность распознавания фото, полноту ответов по накладной или качество русского/казахского диалога. Эти сценарии необходимо выполнить отдельно с действительным OpenAI API key и выбранной моделью.

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

Золотой пример (начните отсюда)

Сквозной прогон за 5 минут: docs/golden-example.md — одна сессия, девять сообщений, требования 1–5 подряд, вложение, казахский язык и отказ от постороннего вопроса. Там же контрольные значения каталога, чек‑лист «что обязано быть в ответе» и полная стенограмма реального прогона от 23.09.2026 с указанием, что именно он не доказывает.

Короткая версия чек‑листа:

№СообщениеТребованиеМашинная проверка
1Есть ли в наличии 010400570_? Характеристики и склады1цена 2 210 ₸, остаток 1352 по пяти складам
2Клемма 030300478_ есть? Если нет — аналог с отличиями2«нет в наличии» + 1–3 аналога с остатком > 0
3Способы оплаты и доставка по Алматы? Минимальная партия?3всё дословно из data/terms.md
4(вложить 01-specification.xlsx) Проверь позиции спецификациивход по ТЗостатки 36, 33, 3, 0; пятый артикул не найден
5Добавь в корзину 3 шт. 030300426_4GET /api/cart → count: 0
6да4GET /api/cart → 3 шт., 1 050 ₸
7Что в моей корзине и где посмотреть?5/cart показывает 030300426_, 3 шт., 1 050 ₸
8Сәлеметсіз бе! 25А үш полюсті автомат бар ма?опциональноответ на казахском, цены из каталога
9Расскажи анекдот про электрикаограниченияотказ одним предложением, корзина не изменилась

Эталонные записи каталога (из data/products.json)

АртикулidТоварЦенаОстатокСклады
010400570_3370412125 АВ (1ф) 25А Schneider Electric (12)2 210 ₸1352Алматы 1182, Нур‑Султан 58, Шымкент (Байдукова) 49, Шымкент (Тассай) 44, Караганда 19
030300426_58378020-108 Коробка распаячная для полых стен с пласт. Лапками и крышкой 110/50.2.2350 ₸3Шымкент (Байдукова) 3
310100080_25397007886 Диф.авт. 1p+N 16А (30мА) 411002 Legrand (1)9 430 ₸0—
010400599_3373325616 УЗО АВДТ (1P+N) 16А (30мА) Schneider Electric (1)33 110 ₸141Алматы 130, Шымкент 5, Караганда 3, …
200300227_515305411002 УЗО АВДТ DX3 (1П+Н) 16А (30мA‑AC) Legrand (1)32 020 ₸50Алматы 31, Нур‑Султан 8, Шымкент 6, …

Сценарии для жюри

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

1. Рабочее требование 1 — наличие и характеристики по артикулу. Вопрос: Есть ли в наличии товар с артикулом 010400570_? Расскажи характеристики и на каких складах он есть. Ожидание: название, цена 2210 ₸, остатки по складам, ссылка на ekt.kz. Пример ответа:

Товар с артикулом 010400570_ — это 12125 АВ (1ф) 25А Schneider Electric (12). Характеристики: цена 2210 тенге, ток 25 А, 1‑фазный. Наличие на складах: Алматы — 1182 шт., Караганда — 19 шт., Нур‑Султан — 58 шт., Шымкент (ул. Байдукова) — 49 шт., Шымкент (Тассай) — 44 шт. Для подробной информации перейдите по ссылке на товар.

2. Рабочее требование 2 — аналог для товара с нулевым остатком. Вопрос: Нужен дифавтомат 1P+N 16А 30мА, артикул 310100080_. Есть в наличии? Если нет — предложи аналог и объясни почему. Ожидание: сообщение об отсутствии и кандидаты в наличии с обоснованием и проверкой отличий. Пример ответа:

К сожалению, дифавтомат 007886 Диф.авт. 1p+N 16А (30мА) 411002 Legrand с артикулом 310100080_ сейчас отсутствует на складах. Вот подходящие альтернативы:

  1. 25616 УЗО АВДТ (1P+N) 16А (30мА) Schneider Electric — 33 110 тг. — с теми же характеристиками… В наличии: Алматы — 130 шт., Шымкент (ул. Байдукова) — 5 шт., Караганда — 3 шт., Нур‑Султан — 2 шт.
  2. 411002 УЗО АВДТ DX3 (1П+Н) 16А (30мA‑AC) Legrand — 32 020 тг. — аналог от того же производителя… В наличии: Алматы — 31 шт., Шымкент (ул. Байдукова) — 6 шт., Нур‑Султан — 8 шт.

3. Рабочее требование 3 — условия покупки из data/terms.md. Вопрос: Какие есть способы оплаты и сколько стоит доставка по Алматы? Ответ (реальный, 23.09.2026):

Физические лица: банковской картой онлайн на сайте; наличными при получении товара; наличными или через POS-терминал в торговом зале при самовывозе. Юридические лица: перечислением на счёт компании по выставленному счёту; наличными в торговом зале при самовывозе (требуются доверенность и удостоверение личности). Доставка по Алматы: в течение 48 часов после согласования заказа. Бесплатная доставка при заказе свыше 15 000 ₸ в черте города; для заказов до 15 000 ₸ — 1 000 ₸ в пределах определённых улиц и 2 000 ₸ за их пределами.

3а. Платёжные данные не принимаются. Вопрос: Можно оплатить прямо здесь в чате? Мой номер карты 4111 1111 1111 1111, CVV 123 Ответ:

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

Номер карты не повторяется в ответе; в логе сервера строка запроса выглядит как Запрос: 79 символов, история: 2 реплик.

3б. Эскалация на менеджера. Вопрос: Нужно 3000 автоматов оптом со скидкой и отсрочкой платежа. С кем это обсудить? Ответ:

Это решает менеджер. Пожалуйста, свяжитесь с ним по телефону [телефон скрыт] или [телефон скрыт], либо напишите на e-mail: [email скрыт].

4. Рабочие требования 4–5 — корзина: количество не превышает остаток, явное подтверждение, ссылка. Вопрос: Добавь в корзину 5 шт. товара с артикулом 030300426_ (остаток в каталоге — 3 шт.). Ответ: ассистент находит товар, вызывает propose_cart_add на доступные 3 шт. и спрашивает:

Я нашёл товар с артикулом 030300426_:

  • 020-108 Коробка распаячная для полых стен с пласт. Лапками и крышкой 110/50.2.2 — 350 тенге — 3 штуки, общая сумма 1050 тенге.

Подтвердите добавление этого товара в корзину?

GET /api/cart в этот момент пуст. После ответа да сервер находит явное согласие в сообщении, confirm_cart_add переносит 3 шт. в корзину:

Товар 020-108 Коробка распаячная для полых стен с пласт. Лапками и крышкой 110/50.2.2 в количестве 3 штук был успешно добавлен в корзину на сумму 1050 тенге. Вы можете просмотреть свою корзину по следующей ссылке: Корзина.

Страница /cart показывает 3 шт. на 1 050 ₸; вопрос «Что у меня в корзине?» отвечается через get_cart. Тот же сценарий с 010400570_ (2 шт., 4 420 ₸) проходит аналогично.

5. Отказ не меняет корзину. Добавь в корзину 1 шт. 010400570_ → предложение с вопросом «Подтвердите добавление?» → нет, не надо → «Добавление товара было отменено». Корзина осталась пустой. Без явного согласия (вопрос, отказ, другое сообщение) состав не меняется.

6. Изоляция корзин. Запрос GET /api/cart из нового приватного окна без cookie или с неизвестным значением cookie возвращает пустую корзину. Сессии независимы; подмена id в теле запроса не выбирает другую корзину. Cookie является ключом доступа: если скопировать действительную cookie другой сессии, сервер не сможет отличить её владельца; пользовательской авторизации нет.

7. Казахский язык. Вопрос: 25А бір полюсті автомат қоймада бар ма? Бағасы қанша? Ожидание: ответ на казахском, с товаром из каталога, ценой и остатками по складам. Замечание: предложен трёхполюсный автомат 027022 АВ DRX125 MT 3ф 25А — поиск по ключевым словам не всегда учитывает число полюсов (см. ограничения).

8. Вопрос не по теме. Расскажи анекдот → одно предложение с вежливым отказом и приглашением выбрать электротовары.

Проверка через API

curl -s http://localhost:8000/chat \
  -H 'Content-Type: application/json' \
  -d '{"message": "Есть ли в наличии 010400570_?", "history": [], "product_ids": []}'

Ответ:

{
  "reply": "Товар с артикулом `010400570_` — это `12125 АВ (1ф) 25А Schneider Electric (12)` ...",
  "products": [
    {"id": 33704, "name": "12125 АВ (1ф) 25А Schneider Electric (12)", "article": "010400570_",
     "price": 2210, "url": "https://ekt.kz/catalog/novinki/.../12125_av_1f_25a_schneider_electric_12/",
     "image": "...", "quantity": 1352}
  ]
}

Пустое сообщение без вложений или набранный текст длиннее 1000 символов возвращают подсказку без обращения к OpenAI. История принимает только реплики {role: "user" | "assistant", content: строка}: не более 10 реплик, 6000 символов в каждой и 20 000 суммарно. Некорректные JSON и поля — HTTP 422 с reply. Ошибка типа вложения — 415, превышение размера файла — 413, повреждённый или пустой документ — 422.

Пример multipart без набранного текста для sh/bash:

curl http://localhost:8000/chat \
  -F 'message=' -F 'history=[]' -F 'product_ids=[]' \
  -F 'files=@docs/attachment-samples/01-specification.xlsx;type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'

В PowerShell используйте curl.exe и запишите команду в одну строку либо замените переносы \ на обратные кавычки.

Проверка корзины через API

Cookie сессии ekt_session выдаётся первым POST /chat; храним её в cookies.txt. Предложение добавления сервер держит у себя, поэтому «да» работает даже с пустой history. Для общего предложения из накладной повторно прикреплять файл перед подтверждением не требуется: подготовленный список хранится в сессии сервера.

curl -s -c cookies.txt -b cookies.txt http://localhost:8000/chat -H 'Content-Type: application/json' \
  -d '{"message": "Добавь в корзину 5 шт. товара с артикулом 030300426_", "history": [], "product_ids": []}'
curl -s -b cookies.txt http://localhost:8000/api/cart        # {"items": [], "count": 0, ...} — до подтверждения пусто
curl -s -c cookies.txt -b cookies.txt http://localhost:8000/chat -H 'Content-Type: application/json' \
  -d '{"message": "да", "history": [], "product_ids": []}'
curl -s -b cookies.txt http://localhost:8000/api/cart        # {"items": [{"article": "030300426_", "qty": 3, "total": 1050, ...}], "total": 1050, ...}
curl -s http://localhost:8000/api/cart                       # без cookie — пустая корзина

Страница для покупателя — http://localhost:8000/cart (та же cookie в браузере).

6. Используемые данные и внешние сервисы

Каталог ekt.kz

  • Источник: тестовый API партнёра https://ekt.kz/api/products?page=N (Basic Auth, 20 позиций на страницу) и карточка товара https://ekt.kz/api/products/detail?id=…. Снимки ответов API лежат в tests/fixtures/api_*.json.
  • Кэш: data/products.json — 400 товаров (первые 20 страниц), снимок от 23.09.2026. В наличии 240 позиций, с нулевым остатком 160; 24 склада (Алматы, Нур‑Султан, Караганда, Шымкент, Тараз, Атырау, Актау, Усть‑Каменогорск, Талдыкорган и др.).
  • Поля записи: id, name, article, price (тенге), url, image, detail.description, detail.quantity, detail.stores[]{name, quantity}, detail.properties (характеристики Bitrix: NOMINALNYY_TOK, NOMINALNOE_NAPRYAZHENIE, KOLICHESTVO_POLYUSOV, KRATNOST_MIN, TORGOVAYA_MARKA, KATEGORIYA и др.). Раздел каталога выводится из URL товара (/catalog/<раздел>/<подраздел>/…). Служебные свойства (CML2_*, SLIDER_PHOTOS и т.п.) отфильтровываются перед передачей модели.
  • Бренды в выборке: Legrand, Schneider Electric, T‑Plast, WAGO, ЗЭТАРУС, DEKraft, UNIT и др.
  • Чего в данных нет: сертификатов, условий оплаты и доставки; поле KATEGORIYA заполнено только у 56 из 400 товаров. Данные партнёра используются как есть; персональных данных в каталоге нет.

OpenAI

  • Chat Completions с function calling; модель из MODEL (по умолчанию gpt-4o-mini), таймаут 30 с, 1 повтор, до 800 токенов ответа, до 5 раундов инструментов на один вопрос.
  • В OpenAI уходят системный промпт, допустимая история диалога, вопрос покупателя, извлечённый текст документов, JPEG/PNG в виде base64 data URL, результаты поиска и состав корзины. Оригинальные PDF/Excel/Word не загружаются в OpenAI.
  • Если покупатель сам включил персональные или платёжные сведения в сообщение, документ или фото, они могут попасть в этот запрос: автоматического удаления таких данных из пользовательского ввода нет. Промпт запрещает их запрашивать и повторять в ответе, но это не гарантия предотвращения передачи. Для проверки используйте обезличенные примеры.

Корзина

  • Хранится в памяти процесса: словарь «id сессии → позиции», TTL 7 дней бездействия, не более 10 000 сессий.
  • Идентификатор сессии (secrets.token_urlsafe) выдаёт сервер и хранит в HttpOnly‑cookie ekt_session; неизвестная cookie заменяется новой.

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

  • data/terms.md — оплата, доставка, возврат, оптовые условия и контакты филиалов, собранные с публичных страниц ekt.kz 23.09.2026. Файл читается при старте и перечитывается при изменении; в промпт попадают первые 6000 символов. Если файла нет, ассистент честно говорит, что условия не заданы, и предлагает обратиться в магазин через сайт.

Тестовые данные

  • tests/fixtures/catalog_small.json — 15 синтетических товаров (в наличии, с нулевым остатком, с «сертификатами», с малым остатком) для офлайн‑тестов;
  • tests/fixtures/terms.md — вымышленные условия покупки для тестов, в приложении не используются.

7. Известные ограничения

  1. Корзина — прототип: хранится в памяти одного процесса сервера (теряется при перезапуске, не разделяется между экземплярами), не интегрирована с корзиной сайта на Bitrix, оформление заказа не реализовано — ссылка ведёт на страницу /cart прототипа. Для реальной интеграции потребуются API корзины магазина, связывание сессий и проверка согласованности остатков. Подтверждение принимается только следующим сообщением после предложения: если покупатель сначала спросил о другом, ассистент повторит предложение и вопрос.
  2. Условия покупки — снимок публичных страниц ekt.kz от 23.09.2026 в data/terms.md, а не официальная выгрузка партнёра; перед показом клиентам файл должен проверить сотрудник магазина. Ассистент отвечает только по нему и отправляет к менеджеру, если ответа в файле нет.
  3. В текущем снимке API нет ссылок на сертификаты — реальные документы из него показать нельзя. Инструмент передаёт ссылки только при их наличии в источнике; синтетические ссылки в тестах не являются сертификатами магазина.
  4. Каталог — статический снимок 400 товаров, обновляется вручную запуском fetch_catalog.py (сервер подхватывает новый файл без перезапуска); в поиске и карточках остатки и цены из снимка. Свежий остаток по API запрашивается только перед подтверждением добавления в корзину. Полный каталог ekt.kz заметно больше, поэтому ассистент говорит «не нашёл в загруженном каталоге», а не «нет в магазине».
  5. Поиск по ключевым словам, а не семантический; точность выбора зависит от модели. В казахском примере среди трёхполюсных автоматов предложены и однополюсные. В длинном диалоге модель иногда не повторяет поиск с in_stock_only=false и говорит «не нашёл в каталоге» о товаре, который есть с нулевым остатком.
  6. Аналоги — кандидаты для сравнения характеристик: при отсутствии KATEGORIYA требуется совпадение полного пути раздела из URL каталога и сходство названий; общего верхнего раздела недостаточно. Если нет подходящих данных или кандидатов, инструмент возвращает пустой список: на снимке каталога от 23.09.2026 аналоги находятся для 57 из 160 товаров с нулевым остатком, для остальных ассистент честно сообщает, что аналогов нет, и отправляет к менеджеру. Найденные совпадения не подтверждают техническую взаимозаменяемость.
  7. Подтверждение проверяется сервером: принимаются короткие явные формулировки согласия, оговорки и другие количества требуют нового предложения. Нестандартную формулировку нужно повторить однозначно. Если запрошено больше остатка или не кратно упаковке, сервер сам собирает предложение на доступное количество и требует назвать уменьшение покупателю — раньше в этом случае предложения не возникало и подтверждать было нечего. При явном согласии сервер принудительно вызывает confirm_cart_add, не полагаясь на выбор модели. Замер 23.09.2026 после правки: одна позиция в пределах остатка, одна позиция с обрезкой количества и три позиции сразу — 4 прогона из 4 в каждом случае.
  8. Ограничения вложений: до 3 файлов по 10 МБ, 8000 символов текста на документ, первые 10 страниц PDF; OCR сканированных PDF не реализован. Word поддерживается в .docx, старый .doc не принимается. Разбор изображений зависит от vision выбранной модели; формулы Excel без сохранённых результатов могут не дать значений. Содержимое вложений не сохраняется в истории браузера — для повторного анализа загрузите файл снова.
  9. История диалога живёт в браузере в рамках открытой вкладки; авторизации пользователей нет.
  10. Нет рекомендаций сопутствующих товаров как отдельной функции; эскалация на менеджера — только текстом с контактами, без передачи диалога в CRM.
  11. CORS по умолчанию открыт (ALLOWED_ORIGINS=*) и нет ограничения частоты запросов — для демонстрации; для сайта задайте ALLOWED_ORIGINS=https://ekt.kz,https://www.ekt.kz. Ссылка на корзину корректна только при заданном PUBLIC_BASE_URL.
  12. Ответ зависит от доступности OpenAI: при таймауте или лимите пользователь получает сообщение «Сервис перегружен, попробуйте через минуту».

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

Развёрнутой версии нет. Демонстрация — локальный запуск или Docker (раздел 4). Репозиторий: https://github.com/BAITC-Hacks/hack-c471837d-jiger.

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

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

aiza1509 19baisaganov 11

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

  • 2026-09-23 17:41Чинит подтверждение корзины и добавляет золотой примерbaisaganov
  • 2026-09-23 17:30Обновить READMEbaisaganov
  • 2026-09-23 17:29Подбирать доступные аналоги с объяснением совпадений и отличийbaisaganov
  • 2026-09-23 17:16Исправить общую корзину, подбор аналогов и ограничения чатаaiza1509
  • 2026-09-23 16:48Новый функционал сертификата, фикс корзиныbaisaganov
  • 2026-09-23 16:44test dataaiza1509
  • 2026-09-23 16:34File attachments functionaiza1509
  • 2026-09-23 16:19Добавить корзину с подтверждением через чат и страницу её составаbaisaganov
Все коммиты (30)
  • 2026-09-23 16:15user cartbaisaganov
  • 2026-09-23 16:06testsaiza1509
  • 2026-09-23 15:47test: analogs for out-of-stock itemsaiza1509
  • 2026-09-23 15:27test: product lookup and not foundaiza1509
  • 2026-09-23 15:20Сохранять полные данные каталога, искать по артикулу и обрабатывать ошибкиbaisaganov
  • 2026-09-23 15:10test: check product detail fieldsaiza1509
  • 2026-09-23 15:07test: check products pagination and fieldsaiza1509
  • 2026-09-23 15:04test: check ekt.kz API authaiza1509
  • 2026-09-23 14:58docs: add Docker instructions to READMEaiza1509
  • 2026-09-23 14:56build: add docker-composeaiza1509
  • 2026-09-23 14:54build: add .dockerignoreaiza1509
  • 2026-09-23 14:52Dockeraiza1509
  • 2026-09-23 14:49Фаза 3: Минималистичный чатbaisaganov
  • 2026-09-23 14:44docs: add test scenarios for Must have 1-5aiza1509
  • 2026-09-23 14:35test: add shared fixtures in conftestaiza1509
  • 2026-09-23 14:31test: add purchase terms fixtureaiza1509
  • 2026-09-23 14:28Фаза 2: Создано ядро проектаbaisaganov
  • 2026-09-23 14:23test: add fixture catalog with in-stock, out-of-stock and analogsaiza1509
  • 2026-09-23 14:16test: add ekt.kz API response samplesaiza1509
  • 2026-09-23 14:10testpytest markers and configaiza1509
  • 2026-09-23 14:05Фаза 1: Добавлен uvbaisaganov
  • 2026-09-23 13:57Фаза 1: Каркасbaisaganov
Структура репозитория (68 файлов)
.dockerignore
.env.example
.gitignore
Dockerfile
README.md
app/__init__.py
app/agent.py
app/analogs.py
app/attachments.py
app/cart.py
app/cart_page.py
app/catalog.py
app/conversation.py
app/main.py
app/prompts.py
app/stock.py
app/terms.py
data/.gitkeep
data/products.json
data/terms.md
docker-compose.yml
docs/attachment-samples.zip
docs/attachment-samples/01-specification.xlsx
docs/attachment-samples/02-invoice.docx
docs/attachment-samples/03-invoice.pdf
docs/attachment-samples/04-legacy-invoice.xls
docs/attachment-samples/05-product.jpg
docs/attachment-samples/06-product.jpeg
docs/attachment-samples/07-invoice.png
docs/attachment-samples/08-long-specification.docx
docs/attachment-samples/09-eleven-pages.pdf
docs/attachment-samples/10-scanned-invoice.pdf
docs/attachment-samples/11-unsupported.txt
docs/attachment-samples/12-corrupted.pdf
docs/attachment-samples/13-empty.pdf
docs/attachment-samples/14-too-large.pdf
docs/attachment-samples/15-confirmation-inside-file.docx
docs/attachment-samples/check-api.ps1
docs/attachment-samples/expected-results.json
docs/attachment-samples/validation-results.json
docs/attachment-samples/КАК-ТЕСТИРОВАТЬ.md
docs/golden-example.md
docs/make_test_cases.py
docs/test-cases.xlsx
fetch_catalog.py
pyproject.toml
requirements.txt
static/chat.html
tests/conftest.py
tests/fixtures/api_product_detail_25397.json
tests/fixtures/api_product_detail_45357.json
tests/fixtures/api_product_detail_58379.json
tests/fixtures/api_products_page1.json
tests/fixtures/capture_samples.py
tests/fixtures/catalog_small.json
tests/fixtures/catalog_small.md
tests/fixtures/terms.md
tests/test_attachments.py
tests/test_cart.py
tests/test_catalog_stock.py
tests/test_ekt_api.py
tests/test_limits.py
tests/test_security.py
tests/test_stock.py
tests/test_terms.py
tests/test_tools_analogs.py
tests/test_tools_product.py
uv.lock

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

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

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