Skip to content
GoatWhistlePublic

About

The KTRU code is issued only when the catalog rules leave exactly one position; otherwise the supplier gets a question, not a guess. Product card builder for Russian public procurement: fills characteristics from a free text description on local models, no paid API

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Портал поставщиков: описание товара своими словами на входе, код КТРУ и характеристики на выходе

Поставщик пишет, что продаёт, как умеет: с опечатками и без половины свойств. Система находит позицию КТРУ, сужает группу до одного кода и заполняет характеристики значениями из справочника.

Запустить локально Доказательства Документация

группа в топ-5: 42 из 46 платные API: не нужны
VRAM: 37,1 из 40 ГБ тесты: 405 бэкенд и 66 фронт лицензия: MIT

фронтенд: React 18 и TypeScript API: FastAPI хранилище: SQLite
поиск: Qwen3-Embedding 0.6B проверка: OpenJev 4B извлечение: Qwen3.5 9B

Зачем это Как это работает Попробовать
Результаты Запуск Устройство Ограничения

Код КТРУ выдаётся, только когда по правилам справочника осталась ровно одна позиция. Модели находят кандидатов и проверяют формулировки, но код выбирают не они: статус «позиция определена» вычисляется из оставшихся позиций, а тест test_indistinct_positions_are_ambiguous_in_catalog падает, если неразличимые позиции получат код.

Зачем это

Чтобы завести карточку на Портале поставщиков, нужно найти позицию КТРУ среди 1 967 позиций трёх категорий и для каждой характеристики выбрать допустимое значение и единицу. В выгрузках СТЕ 299 926 строк, и один товар назван десятками способов: по названию из случайной строки выгрузки поиск ставит правильную группу первой лишь в 23 случаях из 42 (доказательства).

Поставщик пишет «Шкаф для одежды металлический, глубина 0,55 м». Система находит группу «Шкаф для одежды металлический» из двух позиций, исключает одну с причиной «вид товара: секция шкафа; указано шкаф», раскладывает глубину по характеристике с единицей и задаёт один вопрос про тип шкафа. После ответа карточка готова, с кодом 31.01.11.121-00000003.

Как это работает

%%{init: {"theme":"base","themeVariables":{"primaryColor":"#ffffff","primaryTextColor":"#1a1a1a","primaryBorderColor":"#c92723","secondaryColor":"#e7eef7","tertiaryColor":"#f1f5fb","lineColor":"#7f8792","textColor":"#1a1a1a","noteBkgColor":"#e7eef7","noteTextColor":"#5f6672","actorBkg":"#ffffff","actorBorder":"#c92723","fontFamily":"Open Sans"}}}%%
flowchart LR
    A["Описание товара"] --> B["Поиск групп<br/>TF-IDF, векторы, синонимы"]
    B --> C["Проверка назначения<br/>OpenJev"]
    C --> D{"Одна группа<br/>подтверждена?"}
    D -- "нет" --> Q["Вопрос о назначении"]
    D -- "да" --> E["Факты из текста<br/>и их проверка"]
    E --> F["Правила КТРУ<br/>сужают позиции"]
    F --> G{"Позиций осталось"}
    G -- "одна" --> R["Код и карточка"]
    G -- "несколько" --> Q2["Вопрос по различающей<br/>характеристике"]
    G -- "ноль" --> K["Противоречие<br/>с причинами"]
Loading

1. Три метода поиска сливаются в один список

Символьные n-граммы ловят опечатки, векторы Qwen3-Embedding — смысл, словарь синонимов — бытовые названия вроде «болгарки». Ранги сливаются RRF, на проверку уходят десять групп.

R@5 = 42 из 46, поиск групп

2. Модель проверяет назначение, но не выбирает наугад

OpenJev оценивает каждую группу: соответствует, противоречит или нет сведений. Группа выбирается сама, только если подтверждена одна, с оценками не ниже 0,90; иначе поставщику задаётся вопрос.

0 ошибочных автовыборов из 10, доказательства

3. Правила справочника сужают позиции и объясняют каждое исключение

Факт исключает позицию, если противоречит её значению или диапазону с учётом единиц. Неизвестное не исключает. У каждой исключённой позиции — причина словами справочника.

tests/test_rules.py, архитектура

Попробовать за пять минут

После запуска, адрес http://localhost:5173/sell.

Что сделать Что увидите
1 Ввести «Шкаф для одежды металлический, глубина 0,55 м» и нажать «Подобрать позицию» Путь «Мебель › Шкаф для одежды металлический», «Подходит позиций: 1 из 2», вторая позиция исключена с причиной
2 «Продолжить», в строке «Тип шкафа» выбрать «Секционный» Таблица характеристик, «Заполнено обязательных: 2 из 2»
3 «Новая карточка», ввести «Перчатки» Вопрос «Для какого назначения нужен товар?» вместо угаданной группы

Результаты

Сравнение эмбеддеров

Модель Accuracy@1 Hit@5 Hit@20 MRR@20
Qwen3-Embedding-0.6B 62 % 79 % 90 % 0,70
Qwen3-Embedding-4B 77 % 91 % 97 % 0,83
Qwen3-Embedding-8B 79 % 93 % 98 % 0,85

Accuracy@1 — доля запросов с правильным результатом на первом месте; Hit@5 и Hit@20 — доля запросов с правильным результатом в первых 5 и 20 кандидатах. MRR@20 — средний обратный ранг первого правильного результата в топ-20; если его нет, вклад запроса равен нулю.

Проверка решения

Через HTTP API продукта на временной базе, с настоящими моделями на A100 40 ГБ; наборы зафиксированы по хешу, пороги и их источники — в плане валидации.

Метрика Решение База Порог
Правильная группа в топ-5, smoke_v0 42 из 46 42 из 46, TF-IDF и bge-m3 ≥ 42 из 46
Правильная группа в топ-5, реальные СТЕ 27 из 42 26 из 42 без словаря синонимов не задан
Верный выданный код, dev_v1 21 из 24 — ≥ 95 %
Верные автоматически принятые значения 20 из 20 — ≥ 99 %
Ошибочные автовыборы группы 0 из 10 — не задан
Видеопамять всех моделей 37,1 ГБ — ≤ 40 ГБ

Где проигрываем. На реальных названиях из выгрузок правильная группа в топ-5 только у 27 из 42: бренды и артикулы не похожи на наименования групп. Выданный код верен в 88 % случаев при цели 95 %; прогон сделан до подключения проверки назначения.

Пересчитать все числа раздела
backend/.venv/bin/python evals/smoke/bench_search.py --embeddings-url http://127.0.0.1:8081/v1 \
  --database artifacts/ktru-search.sqlite3 --reuse-database
cd backend && uv run python ../evals/e2e/run.py --embeddings-url http://127.0.0.1:8081/v1 && cd ..
backend/.venv/bin/python evals/enum/run.py --url http://127.0.0.1:18080 --threshold 0.90 \
  --holdout evals/datasets/enum_holdout_v2.json --out var/enum-mapping.json
backend/.venv/bin/python evals/groups/run.py --output var/group-validation.json

Нужны сервисы моделей на GPU из запуска. Отчёты пишутся в evals/reports/ и var/; сохранённые отчёты — в evals/reports/.

Запуск

mkdir -p var && cp artifacts/ktru-search.sqlite3 var/tender.db
cd backend && uv sync && uv run alembic upgrade head && uv run uvicorn --factory main:create_app
cd frontend && npm ci --silent && npm run mock-api &
cd frontend && STOREFRONT_URL=http://localhost:8001 API_URL=http://localhost:8000 npm run dev

Интерфейс — http://localhost:5173, Swagger — http://localhost:8000/api/docs. Справочник, витрина и проверки работают без GPU и сети. Анализ описаний требует трёх локальных моделей: EMBEDDINGS_URL, DECISION_URL и EXTRACTOR_URL; без них сессия получает статус технического сбоя. Платные API не нужны.

Модели, проверки, переменные
OPENJEV_PORT=18080 docker compose --profile gpu up --build -d openjev
docker compose -f compose.yaml -f compose.qwen.yaml --profile gpu up -d

Проверки: бэкенд — cd backend && uv run ruff check . && uv run mypy . && uv run pytest, фронтенд — cd frontend && npm run lint && npm run typecheck && npm test && npm run e2e. Переменные окружения, профили GPU и тесты — в запуске и проверках.

Устройство

Интерфейс на React, API на FastAPI с SQLite и три модели в отдельных GPU-сервисах. Ключевое решение — выдать код или задать вопрос — принимают правила справочника в сценарии анализа, а не модели.

Документ Что внутри
Архитектура Части стенда, путь одного сообщения, слои, что заменяется без правки кода
API Методы, ответ анализа, формат ошибок; OpenAPI — /api/docs
Зависимости Модели, библиотеки и сервисы с версиями и лицензиями
Глоссарий Термины КТРУ, работы поставщика и статусы анализа
Оглавление Все страницы документации

Ограничения

  • На реальных названиях поиск слабее. Правильная группа в топ-5 у 27 из 42 строк выгрузки СТЕ; вместо угадывания система спрашивает о назначении.
  • Анализ требует GPU. Три модели занимают 37,1 ГБ на A100 40 ГБ; без них работают справочник, витрина и все тесты, а анализ описаний — нет.
  • Витрина работает на моке. Позиции и характеристики настоящие, цены и поставщики выдуманы.

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


Команда GoatWhistle, Tender Hack, Казань, 2026. Лицензия MIT.

About

The KTRU code is issued only when the catalog rules leave exactly one position; otherwise the supplier gets a question, not a guess. Product card builder for Russian public procurement: fills characteristics from a free text description on local models, no paid API

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages