# TenderLens

Демка · 9 сентября 2026

TenderLens принимает HTML, PDF, XML, JSON и TXT, приводит документы к одному контракту и режет их на стабильные chunks. Индексация идёт через NATS JetStream. bge-m3 в MWS считает embeddings, pgvector ищет близкие фрагменты, а gpt-oss-120b формирует ответ только по найденному контексту и возвращает источники.

Версия исходников: 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec

История реализации: с 20 августа 2026. Публичный стенд: с 7 сентября 2026. Реальные модели MWS подключены 9 сентября 2026.

## Задача

Задача — собрать проверяемый поиск по разнородным тендерным файлам. От исходного документа до ответа остаётся трасса document → chunk → embedding → hit → citation, поэтому результат можно вернуть к конкретному фрагменту.

Поиск и разбор набора документов.

## Выполнено

- Нормализация документов, событийная индексация и хранение chunks в PostgreSQL.
- Search и Ask API с источниками, отдельным no-context ответом и порогом релевантности.
- MWS-провайдер для embeddings и Chat Completions, проверка deployment IDs и размерности векторов.
- Публичный стенд с сессионным и общим rate limit. Ключ MWS остаётся на сервере.

## Что отличает реализацию

Каждый ответ проходит одну трассу: document → chunk → bge-m3 → pgvector → gpt-oss-120b → citation. Повтор события не размножает chunks. Посторонний запрос отсекается до генерации. Корпус синтетический; очередь, индексатор, БД, векторный поиск и обе модели MWS работают реально.

## Состав прикладного кода

- Python / FastAPI: 36.6%
- Python parsers / indexing: 35.2%
- JavaScript / HTML / CSS: 15%
- Python / pgvector: 9.8%
- Python / NATS: 3.4%

Доли по размеру прикладных файлов Git в закреплённой версии. Каждый файл отнесён к одной подсистеме по правилам scripts/generate-stack-share.mjs; тесты, ассеты, зависимости и generated-файлы исключены. Округление — до 0,1%. Это состав кода, а не уровень владения технологиями.

## Стек

| Технология | Роль |
| --- | --- |
| FastAPI / Pydantic | HTTP-контракты, валидация, Search и Ask. |
| MWS Model Hub | bge-m3 для embeddings; gpt-oss-120b для ответа по контексту. |
| NATS JetStream | Доставка событий индексатору и повтор. |
| PostgreSQL / pgvector | Документы, attachments, chunks и cosine search. |
| SQLAlchemy / Python | Нормализация, извлечение, chunking и проверка content_hash. |

## Архитектура

- Seed отправляет TenderChangedV1 через NATS; индексатор извлекает текст и атомарно заменяет chunks при изменении content_hash.
- bge-m3 возвращает 1024-компонентные embeddings. Индексатор пишет имя модели рядом с каждым chunk, а PostgreSQL VECTOR(1024) выполняет cosine search.
- Ask передаёт gpt-oss-120b только найденные фрагменты. Документы в prompt помечены как данные, а не инструкции.
- Порог 0,40 выбран после замера релевантных и посторонних запросов. Если подходящих chunks нет, generation endpoint не вызывается.
- Внешний crawler на публичном стенде выключен. Корпус синтетический, очередь, БД, векторный поиск и MWS inference работают реально.

### Компоненты опубликованного демо

![Компоненты опубликованного демо](https://komaroff-dev.ru/evidence/tenders/diagrams/components.svg)

[Mermaid](https://komaroff-dev.ru/evidence/tenders/diagrams/components.mmd)

### Основной сценарий

![Основной сценарий](https://komaroff-dev.ru/evidence/tenders/diagrams/scenario.svg)

[Mermaid](https://komaroff-dev.ru/evidence/tenders/diagrams/scenario.mmd)

### Размещение опубликованной версии

![Размещение опубликованной версии](https://komaroff-dev.ru/evidence/tenders/diagrams/deployment.svg)

[Mermaid](https://komaroff-dev.ru/evidence/tenders/diagrams/deployment.mmd)

### Состав данных и границы хранения

![Состав данных и границы хранения](https://komaroff-dev.ru/evidence/tenders/diagrams/data.svg)

[Mermaid](https://komaroff-dev.ru/evidence/tenders/diagrams/data.mmd)

### Исходный интеграционный контур; crawler выключен в публичной демке

![Исходный интеграционный контур; crawler выключен в публичной демке](https://komaroff-dev.ru/evidence/tenders/diagrams/original.svg)

[Mermaid](https://komaroff-dev.ru/evidence/tenders/diagrams/original.mmd)

## Code-map

| Функция | Экран / API | Модуль | Данные / проверка |
| --- | --- | --- | --- |
| Начальная индексация | CLI portfolio | cli._upsert_demo_record, NatsBroker, IndexerService | sources, tenders, attachments, chunks |
| Embeddings | Indexer worker | MwsAIProvider.embed, IndexerService | bge-m3, VECTOR(1024), embedding_model |
| Поиск | api/v1/search | SearchService, MwsAIProvider.embed | cosine distance, порог 0,40 |
| Ответ по базе | api/v1/ask | SearchService, MwsAIProvider.generate | найденные chunks, gpt-oss-120b, citations |
| Повтор события | api/demo/replay | NatsBroker / content_hash guard | общий read-only корпус |
| Сессия и лимиты | cookie / middleware | portfolio, auth, rate_limit | api_keys, 5/session, 12/process |

Перед изменением пройти связи экран → API → состояние → хранилище. Проверить входы, ошибки и повторные действия. Обновить карту и схемы вместе с кодом, выполнить связанные проверки и привязать материалы к версии.

## Сценарии

- Найдите документ о серверах: первый результат должен содержать гарантию 36 месяцев и метку bge-m3.
- Переключитесь на Ask: gpt-oss-120b должен ответить по найденным фрагментам и показать источники.
- Выберите «Нет контекста»: при score ниже 0,40 сервис возвращает отказ без вызова модели генерации.
- Проверьте состояние корпуса: четыре документа, 16 chunks и ready у каждой записи.

## Проверки

- Black, Flake8 и MyPy пройдены для версии 6c005b6.
- 129 unit/API тестов пройдены; MkDocs собран в strict режиме, сгенерированный code reference актуален.
- Production-образ проверен с реальными MWS endpoints: readiness, embedding 1024 и grounded Ask.
- В опубликованной версии Search, Ask и no-context прошли через внешний HTTPS; ошибок console и failed requests нет.
- Viewport 390 px проверен без горизонтального overflow.
- GitHub workflow для этой ветки не запускался; указанные проверки выполнены локально и на опубликованном стенде.

[CI](https://github.com/komaroffsergei/tender-lens/actions/runs/33046876189) — 84c37f0f6b52fef9ec0eddb11078ebed0b5aef26, 2026-08-27T06:43:00Z; исторический запуск.

[Documentation](https://github.com/komaroffsergei/tender-lens/actions/runs/33046876185) — 84c37f0f6b52fef9ec0eddb11078ebed0b5aef26, 2026-08-27T06:43:00Z; исторический запуск.

## Результаты

- Опубликованы десять кадров версии 6c005b6: desktop, mobile, Search, Ask, no-context и validation.
- Четыре ready-документа переиндексированы в 16 векторов bge-m3 размерности 1024.
- В галерейной browser-проверке 9 сентября 2026 desktop Search занял 298 мс, Ask — 1145 мс, no-context — 115 мс; mobile Search — 137 мс, Ask — 1936 мс. Это одиночные измерения, не benchmark.
- Для релевантных тестовых запросов top cosine был 0,604–0,927; для двух посторонних — 0,278 и 0,370. Production threshold выставлен 0,40.
- Текущий снимок двух контейнеров TenderLens: около 121,4 MiB RAM суммарно при простое. Модели выполняются в MWS и не занимают RAM/GPU VPS.
- На момент снимка 9 сентября 2026 было доступно около 5,05 GiB RAM и 105,7 GiB диска на VPS.

![Контейнеры TenderLens: RAM при простое и лимит · 9 сентября 2026, одиночный снимок](https://komaroff-dev.ru/evidence/tenders/charts/mws-runtime.svg)

[Данные](https://komaroff-dev.ru/evidence/tenders/metrics/mws-runtime.json)

![Время запросов через HTTPS и MWS · 9 сентября 2026, по одному запуску](https://komaroff-dev.ru/evidence/tenders/charts/mws-request-time.svg)

[Данные](https://komaroff-dev.ru/evidence/tenders/metrics/browser-qa.json)

## Ограничения

- Четыре синтетических документа и 16 chunks — небольшой демонстрационный корпус, а не база реальных закупок.
- Качество модели на независимом наборе не измерялось. Показанные cosine scores описывают только запросы из проверки 9 сентября 2026.
- Лимит — 5 Search/Ask запросов в минуту на сессию и 12 запросов в минуту на весь процесс. Запросы пользователей не сохраняются.
- Inference зависит от доступности двух deployment в MWS. При недоступности провайдера readiness становится красным, автоматического перехода на fake нет.
- Worker health подтверждает соединение при старте; отдельный детектор зависшего вычисления не реализован.

## Запуск и эксплуатация

Публичная версия работает в AI_MODE=mws. API и indexer обращаются к OpenAI-совместимым MWS endpoints по HTTPS. API key передаётся контейнерам как server-side secret и не попадает в браузер, образ или Git.

bge-m3 используется и при индексации, и при запросе. После смены embedding deployment весь корпус нужно переиндексировать: векторы разных моделей в одном индексе не смешиваются. Readiness проверяет список доступных deployment IDs.

Dockerfile.portfolio собирается вне VPS; API и worker используют один закреплённый образ с отдельными лимитами. PostgreSQL и NATS доступны только во внутренней Docker-сети, API опубликован через Nginx.

После миграций CLI seed создаёт четыре синтетических документа. Повтор события безопасен благодаря content_hash. После рестарта проверяются ready у четырёх документов, 16 chunks, модель bge-m3 и размерность 1024.

Сессионный лимит равен пяти запросам в текущую UTC-минуту, общий лимит процесса — двенадцати. No-context запрос останавливается до Chat Completions. Пользовательские запросы не пишутся в БД.

Откат сохраняет предыдущий образ и release-конфигурацию. Смена embedding provider требует обратной переиндексации. Общий docker prune не используется.

Code-map связывает экран или API с модулем и данными. При правке проверяются обычный запрос, no-context, rate limit, повтор события, readiness и отсутствие ключа MWS в клиентских файлах и логах.

## Скриншоты

[Опубликованный стенд с реальными моделями MWS, pgvector, NATS и синтетическим корпусом](https://komaroff-dev.ru/evidence/tenders/full/01-overview-desktop.png) — 2026-09-09T11:36:17.259Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Состояние индексатора: четыре готовых документа и 16 векторов bge-m3](https://komaroff-dev.ru/evidence/tenders/full/02-index-status-desktop.png) — 2026-09-09T11:36:17.380Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Семантический поиск через bge-m3 и точное сравнение векторов в pgvector](https://komaroff-dev.ru/evidence/tenders/full/03-search-bge-m3-desktop.png) — 2026-09-09T11:36:17.921Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Первый найденный фрагмент с cosine score, именем файла и ссылкой на источник](https://komaroff-dev.ru/evidence/tenders/full/04-result-detail-desktop.png) — 2026-09-09T11:36:18.033Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Ответ gpt-oss-120b по найденному контексту с пятью показанными источниками](https://komaroff-dev.ru/evidence/tenders/full/05-grounded-answer-mws-desktop.png) — 2026-09-09T11:36:19.347Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Порог 0,40 отсекает посторонний запрос, поэтому генерация ответа не запускается](https://komaroff-dev.ru/evidence/tenders/full/06-no-context-desktop.png) — 2026-09-09T11:36:19.662Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Клиентская проверка короткого запроса до обращения к API и внешней модели](https://komaroff-dev.ru/evidence/tenders/full/07-validation-desktop.png) — 2026-09-09T11:36:19.832Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Полная мобильная раскладка стенда на viewport 390 пикселей](https://komaroff-dev.ru/evidence/tenders/full/08-overview-mobile.png) — 2026-09-09T11:36:20.795Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Мобильные карточки результата поиска Angular и TypeScript через bge-m3](https://komaroff-dev.ru/evidence/tenders/full/09-search-mobile.png) — 2026-09-09T11:36:21.093Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Мобильный ответ MWS по базе с источниками и текущим лимитом сессии](https://komaroff-dev.ru/evidence/tenders/full/10-answer-mobile.png) — 2026-09-09T11:36:23.141Z, 6c005b6ef86329e7d74e9620058f55b5c6e7d4ec.

[Материалы](https://komaroff-dev.ru/downloads/tenders-evidence.zip)
