О проекте
Команда: 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 |
| LLM | OpenAI 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
Поток одного запроса
- Браузер отправляет
POST /chatкакFormData: текст вопроса, историю, id показанных товаров и вложения. Cookieekt_sessionидентифицирует корзину (выдаётся сервером при первом запросе). JSON-запросы без файлов также поддерживаются. - Сервер проверяет файлы, извлекает текст документов и формирует блоки «Содержимое вложения «имя файла»: …». Изображения кодируются в base64 data URL и передаются в
image_url-частях сообщения. Вложения доступны модели в запросе с загрузкой; браузер сохраняет в истории набранный текст, имена файлов и ответ, но не содержимое файлов. Для нового анализа документа его нужно приложить повторно. - Сервер собирает системный промпт: правила консультанта, условия покупки и контакты из
data/terms.md, данные уже показанных товаров, текущий состав корзины и ожидающее подтверждения предложение, подсказка языка. - Модель вызывает инструменты каталога (
search_productsс фильтрами,get_product,list_categories,find_analogs) и корзины. Несколько вызовов инструментов могут быть запрошены в одном раунде. - Результаты инструментов возвращаются модели; после не более чем 5 раундов модель формирует текстовый ответ.
- Сервер отбирает товары, упомянутые в ответе, и отдаёт
{"reply": "...", "products": [...]}. - Фронтенд экранирует значения при рендеринге Markdown‑ответа и показывает карточки товаров с фото, ценой и остатком.
Инструменты модели (function calling)
| Инструмент | Аргументы | Что делает |
|---|---|---|
search_products | query, max_results (1–20), in_stock_only, min_price, max_price, category, offset | Поиск по артикулу и ключевым словам с фильтрами; возвращает total, offset, items (id, название, артикул, цена, категория, ссылка, фото, остаток, in_stock, склады с остатком > 0). Точный артикул → первым, затем товары в наличии, затем по релевантности и цене. |
list_categories | — | Разделы каталога (два уровня из URL товара) с числом товаров и числом в наличии. |
get_product | product_id | Полная карточка: описание, характеристики (properties), минимальная партия (min_batch), остатки по складам и certificate_urls из исходных данных. |
find_analogs | sku | До 5 товаров той же категории с остатком > 0, отсортированных по сходству описаний, с полем reason. |
propose_cart_add | product_id, quantity | Шаг 1: проверяет товар, остаток, уже лежащее в корзине количество и кратность. Вызовы для разных товаров в одном ходе собирают общее предложение; повтор того же id заменяет его количество. При нарушении возвращает error с max_quantity / suggested_quantity. Корзина не меняется. |
confirm_cart_add | — | Шаг 2: после явного согласия без оговорок в следующем сообщении перепроверяет все позиции и атомарно переносит предложение в корзину либо возвращает ошибку без добавления. Возвращает результат и cart_url. |
cancel_cart_proposal | — | Снимает неподтверждённое предложение. |
get_cart | — | Текущий состав корзины, суммы и ссылка. |
HTTP‑интерфейс
| Метод и путь | Назначение |
|---|---|
GET / | Интерфейс чата |
POST /chat | Multipart: текстовое message, поля history и product_ids как JSON-строки, повторяющееся поле files; либо прежний JSON {message, history, product_ids}. Ответ: {reply, products}. Выдаёт cookie сессии |
GET /cart | HTML‑страница корзины текущей сессии |
GET /api/cart | Состав корзины в JSON |
GET /docs | Swagger‑документация |
Состав репозитория
| Путь | Назначение |
|---|---|
app/main.py | FastAPI‑приложение: маршруты, 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__.py | redact_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_ | 4 | GET /api/cart → count: 0 |
| 6 | да | 4 | GET /api/cart → 3 шт., 1 050 ₸ |
| 7 | Что в моей корзине и где посмотреть? | 5 | /cart показывает 030300426_, 3 шт., 1 050 ₸ |
| 8 | Сәлеметсіз бе! 25А үш полюсті автомат бар ма? | опционально | ответ на казахском, цены из каталога |
| 9 | Расскажи анекдот про электрика | ограничения | отказ одним предложением, корзина не изменилась |
Эталонные записи каталога (из data/products.json)
| Артикул | id | Товар | Цена | Остаток | Склады |
|---|---|---|---|---|---|
010400570_ | 33704 | 12125 АВ (1ф) 25А Schneider Electric (12) | 2 210 ₸ | 1352 | Алматы 1182, Нур‑Султан 58, Шымкент (Байдукова) 49, Шымкент (Тассай) 44, Караганда 19 |
030300426_ | 58378 | 020-108 Коробка распаячная для полых стен с пласт. Лапками и крышкой 110/50.2.2 | 350 ₸ | 3 | Шымкент (Байдукова) 3 |
310100080_ | 25397 | 007886 Диф.авт. 1p+N 16А (30мА) 411002 Legrand (1) | 9 430 ₸ | 0 | — |
010400599_ | 33733 | 25616 УЗО АВДТ (1P+N) 16А (30мА) Schneider Electric (1) | 33 110 ₸ | 141 | Алматы 130, Шымкент 5, Караганда 3, … |
200300227_ | 515305 | 411002 УЗО АВДТ 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_сейчас отсутствует на складах. Вот подходящие альтернативы:
25616 УЗО АВДТ (1P+N) 16А (30мА) Schneider Electric— 33 110 тг. — с теми же характеристиками… В наличии: Алматы — 130 шт., Шымкент (ул. Байдукова) — 5 шт., Караганда — 3 шт., Нур‑Султан — 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‑cookieekt_session; неизвестная cookie заменяется новой.
Условия покупки и контакты
data/terms.md— оплата, доставка, возврат, оптовые условия и контакты филиалов, собранные с публичных страниц ekt.kz 23.09.2026. Файл читается при старте и перечитывается при изменении; в промпт попадают первые 6000 символов. Если файла нет, ассистент честно говорит, что условия не заданы, и предлагает обратиться в магазин через сайт.
Тестовые данные
tests/fixtures/catalog_small.json— 15 синтетических товаров (в наличии, с нулевым остатком, с «сертификатами», с малым остатком) для офлайн‑тестов;tests/fixtures/terms.md— вымышленные условия покупки для тестов, в приложении не используются.
7. Известные ограничения
- Корзина — прототип: хранится в памяти одного процесса сервера (теряется при перезапуске, не разделяется между экземплярами), не интегрирована с корзиной сайта на Bitrix, оформление заказа не реализовано — ссылка ведёт на страницу
/cartпрототипа. Для реальной интеграции потребуются API корзины магазина, связывание сессий и проверка согласованности остатков. Подтверждение принимается только следующим сообщением после предложения: если покупатель сначала спросил о другом, ассистент повторит предложение и вопрос. - Условия покупки — снимок публичных страниц ekt.kz от 23.09.2026 в
data/terms.md, а не официальная выгрузка партнёра; перед показом клиентам файл должен проверить сотрудник магазина. Ассистент отвечает только по нему и отправляет к менеджеру, если ответа в файле нет. - В текущем снимке API нет ссылок на сертификаты — реальные документы из него показать нельзя. Инструмент передаёт ссылки только при их наличии в источнике; синтетические ссылки в тестах не являются сертификатами магазина.
- Каталог — статический снимок 400 товаров, обновляется вручную запуском
fetch_catalog.py(сервер подхватывает новый файл без перезапуска); в поиске и карточках остатки и цены из снимка. Свежий остаток по API запрашивается только перед подтверждением добавления в корзину. Полный каталог ekt.kz заметно больше, поэтому ассистент говорит «не нашёл в загруженном каталоге», а не «нет в магазине». - Поиск по ключевым словам, а не семантический; точность выбора зависит от модели. В казахском примере среди трёхполюсных автоматов предложены и однополюсные. В длинном диалоге модель иногда не повторяет поиск с
in_stock_only=falseи говорит «не нашёл в каталоге» о товаре, который есть с нулевым остатком. - Аналоги — кандидаты для сравнения характеристик: при отсутствии
KATEGORIYAтребуется совпадение полного пути раздела из URL каталога и сходство названий; общего верхнего раздела недостаточно. Если нет подходящих данных или кандидатов, инструмент возвращает пустой список: на снимке каталога от 23.09.2026 аналоги находятся для 57 из 160 товаров с нулевым остатком, для остальных ассистент честно сообщает, что аналогов нет, и отправляет к менеджеру. Найденные совпадения не подтверждают техническую взаимозаменяемость. - Подтверждение проверяется сервером: принимаются короткие явные формулировки согласия, оговорки и другие количества требуют нового предложения. Нестандартную формулировку нужно повторить однозначно. Если запрошено больше остатка или не кратно упаковке, сервер сам собирает предложение на доступное количество и требует назвать уменьшение покупателю — раньше в этом случае предложения не возникало и подтверждать было нечего. При явном согласии сервер принудительно вызывает
confirm_cart_add, не полагаясь на выбор модели. Замер 23.09.2026 после правки: одна позиция в пределах остатка, одна позиция с обрезкой количества и три позиции сразу — 4 прогона из 4 в каждом случае. - Ограничения вложений: до 3 файлов по 10 МБ, 8000 символов текста на документ, первые 10 страниц PDF; OCR сканированных PDF не реализован. Word поддерживается в
.docx, старый.docне принимается. Разбор изображений зависит от vision выбранной модели; формулы Excel без сохранённых результатов могут не дать значений. Содержимое вложений не сохраняется в истории браузера — для повторного анализа загрузите файл снова. - История диалога живёт в браузере в рамках открытой вкладки; авторизации пользователей нет.
- Нет рекомендаций сопутствующих товаров как отдельной функции; эскалация на менеджера — только текстом с контактами, без передачи диалога в CRM.
- CORS по умолчанию открыт (
ALLOWED_ORIGINS=*) и нет ограничения частоты запросов — для демонстрации; для сайта задайтеALLOWED_ORIGINS=https://ekt.kz,https://www.ekt.kz. Ссылка на корзину корректна только при заданномPUBLIC_BASE_URL. - Ответ зависит от доступности OpenAI: при таймауте или лимите пользователь получает сообщение «Сервис перегружен, попробуйте через минуту».
8. Ссылка на развёрнутую версию
Развёрнутой версии нет. Демонстрация — локальный запуск или Docker (раздел 4). Репозиторий: https://github.com/BAITC-Hacks/hack-c471837d-jiger.
Авторы коммитов
История коммитов
- 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 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.