chore: форматирование модулей + план по шагам

This commit is contained in:
Repinoid
2026-10-07 14:11:35 +03:00
parent 2136536b0d
commit c59f0245b5
6 changed files with 808 additions and 479 deletions
+162
View File
@@ -0,0 +1,162 @@
# Пошаговый план реализации AI-ассистента документации
Дата: 2026-10-07
Кому: субагент DeepSeek Flash (исполнитель механических шагов)
Кто отвечает за интеграцию/проверку: ассистент (я)
Контекст: см. `BRIEF_docs_ai_assistant.md` (решения), `reviews/` (рецензия + диалог).
Рабочий каталог кода: `~/TF/tf_docs` (Node.js, без внешних зависимостей, node >= 18).
> КАЖДЫЙ шаг ниже — отдельная порция для субагента Flash.
> Flash делает механику, я (ассистент) принимаю/интегрирую/проверяю.
> После каждого шага — коммит осмысленным сообщением, push только по команде владельца.
---
## Шаг 0. Блокеры и рекомендации (утвердить у владельца)
**Не код. Нужны решения владельца до начала.**
- Блокеры: 7.1 (путь `/<stand>/_api/assist`), 7.2 (политика отправки данных в ЛЛМ), 7.3 (`X-Forwarded-For`/`ASSIST_TRUSTED_PROXY_CIDR`), 7.5 (не снят — уточнить), ротация ключа `api.aillm.ru`.
- Рекомендации одним списком: 7.4, 7.6, 7.7, 7.8, 7.9, 7.11.
- Приёмник: `llm_project/DECISIONS_v0.md` (фиксируем принятое).
---
## Шаг 1. Live-проверка `api.aillm.ru`
**Задача Flash:** написать одноразовый скрипт-пробник (НЕ часть `assist.js`) и вернуть факты.
Файл: `llm_project/probe/probe_aillm.js` (запуск вручную, результат — в `llm_project/probe/RESULTS.md`).
Проверить и зафиксировать:
1. `response_format: {type: "json_object"}` — поддерживается? (даёт ли валидный JSON)
2. `stream: true` — какой формат чанков (`data:` SSE)? нужен ли нам вообще (v1 — нет).
3. `reasoning_effort: "low"` — принимается? есть ли `reasoning_content` в ответе?
4. Ответ на 429 — какие заголовки (`Retry-After`? `X-RateLimit-*`?), тело.
5. Размер контекста — до скольки токенов принимает запрос (увеличивая промпт, найти предел/ошибку).
6. Формат ошибки при пустом `content` / обрыве.
7. Сколько длится этап 1-типа запрос (засечь 3 образца) — для дедлайна 40 с.
**Критерий готово:** `RESULTS.md` содержит таблицу ответов по всем 7 пунктам + пример сырого ответа (JSON) для этапов 1 и 2.
**После шага 1:** финализируем `llmChat` (структуру разбора ответа).
---
## Шаг 2. Каркас `assist.js`
**Задача Flash:** создать `assist.js` (новый модуль), НЕ трогая `server.js`.
Модули по брифу:
- `validateInput(req)` — Content-Type `application/json`, Content-Length > 2048 → 413, чтение потока со счётчиком байт (> 2 КБ → 413), разбор JSON, поле `question` (непустая строка).
- rate-limit — локальная таблица ключ=IP (или IP/stand), периодическая чистка; лимит делится на число реплик (`ASSIST_REPLICAS`).
- семафор `ASSIST_MAX_CONCURRENT` = 4; сверх — 503 + `Retry-After: 5`, тело `{"error":"busy","message":"..."}`.
- объединение одинаковых запросов (ключ = `stand` + нормализованный запрос): два счётчика («ожидающие клиенты» / «держащие вызов»); обрыв клиента не отменяет общий вызов, пока есть держащие; отмена ЛЛМ только когда отключились все.
- `llmChat(messages, {max_tokens, controller})` — POST на `api.aillm.ru`, `AbortController`, разбор только `content`.
- общий дедлайн `ASSIST_TOTAL_DEADLINE_MS` = 40000; повтор этапа 1 только если запас > 15 с; `req.on('close')` → abort.
- кэш ответов (ключ = `stand` + нормализованный запрос), TTL.
- логирование: длина вопроса, `stand`, время этапов, статус, кэш, токены. Текст запросов — НЕ логировать.
**Критерий готово:** модуль экспортирует обработчик, собирается без ошибок (`node -c assist.js`), покрыт беглыми юнит-пробами в `test/` (validateInput, семафор, дедуп-ключ).
---
## Шаг 3. Эталонный набор вопросов
**Задача Flash:** создать `llm_project/golden/golden_questions.json` — 20–30 вопросов.
Формат каждой записи:
```json
{"question": "...", "expected_pages": ["path/на_страницу"], "resource_type": "nubes_..."}
```
Вопросы: по разным ресурсам (`nubes_*`), включая подресурсы (`_user`, `_database`, `_topic`), негативные (вопрос не про ресурс, вопрос с выдуманным ресурсом вроде `nubes_instance`).
**Критерий готово:** файл валиден, вопросы реалистичны (взяты из опубликованной документации стенда `nubes-test`).
---
## Шаг 4. Индекс и поиск
**Задача Flash:** реализовать загрузку и лексический поиск по `search_index.json`.
- Источник: `VM_DOCS_BASE_URL/<stand>/search/search_index.json` (только этот путь).
- Снять формат индекса с реального стенда (см. шаг 7/8 верификации): структура записи, поля `location` (вид `page/#anchor`), `title`, `text` (HTML → удалить).
- Лекс. поиск: токенизация, BM25-подобный скор, top-K разделов, группировка по страницам.
- Собрать список реальных `nubes_*` из имён страниц (для этапа 1).
- Порог «не найдено» = «совпало >= N уникальных ключевых слов», N подобрать по эталону (шаг 3).
**Критерий готово:** на эталонном наборе поиск выдаёт ожидаемые страницы с приемлемой точностью (записать метрику).
---
## Шаг 5. Этап 1 (извлечение `resource_type`)
**Задача Flash:** реализовать этап 1.
- Системный промпт (рус.) со списком реальных `nubes_*` из индекса.
- Ответ: `resource_type`; если не в списке — отбрасывается (нет ресурса).
- Падение/таймаут этапа 1 — продолжить без него (только токен-поиск).
**Критерий готово:** на эталоне корректно извлекает/отбрасывает `resource_type`; выдуманные (`nubes_instance`) — отбрасываются.
---
## Шаг 6. Объединение + этап 2
**Задача Flash:** реализовать объединение (П.8) и этап 2.
Объединение:
- Поиск по токенам исходного запроса — наравне с поиском после этапа 1.
- Страница ресурса — всегда первой в контекст (если `resource_type` в списке).
- Кандидаты объединяются, повторы по `location`, оценка = max из двух; контекст до K без повторов; страница ресурса входит в K.
- Падение этапа 1 — только токен-поиск, без обязательной страницы.
Этап 2:
- Промпт со строгим шаблоном «Роль/Задача/Контекст/Требования/Ограничения».
- Ответ без заголовков → 502 `bad_llm_output`.
- Жёсткий `max_tokens`.
- `sources` = `/<stand>/` + `location`; отбрасывать `//`, `:`, `..`.
**Критерий готово:** полный сквозной ответ на эталоне, корректные `sources`.
---
## Шаг 7. Маршрут в `server.js` + Dockerfile + README
**Задача Flash (под контролем):** в `server.js` добавить POST-ветку `/<stand>/_api/assist` **до** проверки 405 (строки 72–75).
- `stand` через `STAND_RE`.
- `assist.js` грузить в try/catch; ошибки → 500 JSON.
- Поведение статики и падение без `VM_DOCS_BASE_URL` — НЕ менять.
- Dockerfile: скопировать `assist.js` (+ при необходимости `lib/`).
- README: описать эндпоинт и переменные окружения (`ASSIST_*`, ключ).
**Критерий готово:** `node -c`, статика работает как раньше, POST отвечает JSON (200/4xx/5xx).
---
## Шаг 8. Виджет (фронтенд)
**Задача Flash:** написать виджет (встраиваемый JS/CSS/HTML или шаблон).
- `stand` из первого сегмента `location.pathname`; нет сегмента — не показывать.
- Плавающая кнопка на всех страницах.
- Под полем ввода: «не вводите ключи и пароли».
- Обращение к `/<stand>/_api/assist`.
- Обработка 503 (`Retry-After`) — показ «занято, повторите».
**Критерий готово:** виджет открывается, шлёт запрос, показывает ответ и `sources`-ссылки.
---
## Шаг 9. Итоговая оценка качества
**Задача Flash:** прогнать эталонный набор (шаг 3) через полный пайплайн, собрать метрики.
- Точность `resource_type`, точность страниц, корректность `sources`, % 502/ошибок, задержки.
**Критерий готово:** отчёт `llm_project/golden/EVAL_*.md` с таблицей.
---
## Шаг 10. Деплой — ТОЛЬКО по команде владельца. Ассистент сам НЕ деплоит.
## Шаг 11. Документация (README финал, переменные, эксплуатация).
---
## Порядок выполнения и зависимости
1. Шаг 0 (утверждение) → блокер, ждать владельца.
2. Шаг 1 и Шаг 2 — параллельно (каркас не зависит от результата шага 1; `llmChat` финализируется после шага 1).
3. Шаг 3 → 4 → 5 → 6 (зависимы).
4. Шаг 7 → 8 (после 6).
5. Шаг 9 (оценка), Шаг 10 (деплой, по команде), Шаг 11 (документация).
## Непроверенные допущения (снять на стенде)
- `proxy-read-timeout` nginx-ingress (шаг 1/верификация).
- Формат `search_index.json` (шаг 4).
- Пропускает ли ingress POST (`curl -X POST` на стенде) — отдельная верификация.
- Поддержка `reasoning_effort`/`response_format`/`stream`/429 у `api.aillm.ru` (шаг 1).
+195 -28
View File
@@ -1,30 +1,197 @@
[ [
{"question": "Как создать виртуальную машину в облаке Nubes?", "expected_pages": ["vc_vm_v3"], "resource_type": "nubes_vc_vm_v3"}, {
{"question": "Как настроить PostgreSQL с автоматическим бэкапом?", "expected_pages": ["postgres"], "resource_type": "nubes_postgres"}, "question": "Как создать виртуальную машину в облаке Nubes?",
{"question": "Как создать базу данных и пользователя в PostgreSQL?", "expected_pages": ["postgres"], "resource_type": "nubes_postgres_database"}, "expected_pages": [
{"question": "Как выдать виртуальной машине белый IP-адрес?", "expected_pages": ["vcexternalip"], "resource_type": "nubes_vcexternalip"}, "vc_vm_v3"
{"question": "Как поднять MongoDB с ежедневным бэкапом?", "expected_pages": ["mongodb"], "resource_type": "nubes_mongodb"}, ],
{"question": "Как завести пользователя и пароль для MongoDB?", "expected_pages": ["mongodb"], "resource_type": "nubes_mongodb_user"}, "resource_type": "nubes_vc_vm_v3"
{"question": "Как развернуть Redis для кэширования?", "expected_pages": ["redis"], "resource_type": "nubes_redis"}, },
{"question": "Как создать MariaDB и привязать к ней отдельную базу?", "expected_pages": ["mariadb"], "resource_type": "nubes_mariadb"}, {
{"question": "Как поднять ClickHouse и создать в нём таблицу?", "expected_pages": ["clickhouse"], "resource_type": "nubes_clickhouse_database"}, "question": "Как настроить PostgreSQL с автоматическим бэкапом?",
{"question": "Как развернуть Kafka-кластер и создать топик?", "expected_pages": ["kafka"], "resource_type": "nubes_kafka_topic"}, "expected_pages": [
{"question": "Как завести пользователя Kafka с правами на конкретный топик?", "expected_pages": ["kafka"], "resource_type": "nubes_kafka_user"}, "postgres"
{"question": "Как развернуть RabbitMQ для очередей задач?", "expected_pages": ["rabbitmq"], "resource_type": "nubes_rabbitmq"}, ],
{"question": "Как создать S3-бакет для хранения файлов?", "expected_pages": ["s3bucket", "s3"], "resource_type": "nubes_s3bucket"}, "resource_type": "nubes_postgres"
{"question": "Как создать S3-хранилище и сублюзеров с ограниченными ключами?", "expected_pages": ["s3"], "resource_type": "nubes_s3_sub_user"}, },
{"question": "Как развернуть Kubernetes-кластер в Shturval?", "expected_pages": ["k8s_sthutrval_cluster"], "resource_type": "nubes_k8s_sthutrval_cluster"}, {
{"question": "Как установить Velero для бэкапа Kubernetes?", "expected_pages": ["k8s_velero"], "resource_type": "nubes_k8s_velero"}, "question": "Как создать базу данных и пользователя в PostgreSQL?",
{"question": "Как развернуть OpenBao для хранения секретов в кластере?", "expected_pages": ["k8s_openbao"], "resource_type": "nubes_k8s_openbao"}, "expected_pages": [
{"question": "Как создать DNS-запись для своего домена?", "expected_pages": ["dnsrecord", "zones_v2"], "resource_type": "nubes_dnsrecord"}, "postgres"
{"question": "Как создать организацию и vDC в VMware-облаке?", "expected_pages": ["vc_org", "vc_vdc"], "resource_type": "nubes_vc_vdc"}, ],
{"question": "Как настроить SNAT в NSX-T?", "expected_pages": ["vc_nsxt"], "resource_type": "nubes_vc_nsxt_snat"}, "resource_type": "nubes_postgres_database"
{"question": "Как развернуть LLM-модель с секретами и командой?", "expected_pages": ["llm_ai"], "resource_type": "nubes_llm_ai"}, },
{"question": "Как развернуть Nextcloud и настроить бэкап?", "expected_pages": ["nextcloud"], "resource_type": "nubes_nextcloud"}, {
{"question": "Как развернуть Node.js приложение и сделать передеплой?", "expected_pages": ["nodejs"], "resource_type": "nubes_nodejs"}, "question": "Как выдать виртуальной машине белый IP-адрес?",
{"question": "Как задеплоить Flask-приложение и получить его URL?", "expected_pages": ["flask"], "resource_type": "nubes_flask"}, "expected_pages": [
{"question": "Как развернуть pgAdmin для управления базами?", "expected_pages": ["pgadmin"], "resource_type": "nubes_pgadmin"}, "vcexternalip"
{"question": "Как подключиться к API Terraform-провайдера Nubes?", "expected_pages": [], "resource_type": ""}, ],
{"question": "Какие версии провайдера сейчас доступны и как их обновлять?", "expected_pages": ["template"], "resource_type": ""}, "resource_type": "nubes_vcexternalip"
{"question": "Как создать nubes_instance с плавающим IP floating_ip?", "expected_pages": [], "resource_type": ""} },
{
"question": "Как поднять MongoDB с ежедневным бэкапом?",
"expected_pages": [
"mongodb"
],
"resource_type": "nubes_mongodb"
},
{
"question": "Как завести пользователя и пароль для MongoDB?",
"expected_pages": [
"mongodb"
],
"resource_type": "nubes_mongodb_user"
},
{
"question": "Как развернуть Redis для кэширования?",
"expected_pages": [
"redis"
],
"resource_type": "nubes_redis"
},
{
"question": "Как создать MariaDB и привязать к ней отдельную базу?",
"expected_pages": [
"mariadb"
],
"resource_type": "nubes_mariadb"
},
{
"question": "Как поднять ClickHouse и создать в нём таблицу?",
"expected_pages": [
"clickhouse"
],
"resource_type": "nubes_clickhouse_database"
},
{
"question": "Как развернуть Kafka-кластер и создать топик?",
"expected_pages": [
"kafka"
],
"resource_type": "nubes_kafka_topic"
},
{
"question": "Как завести пользователя Kafka с правами на конкретный топик?",
"expected_pages": [
"kafka"
],
"resource_type": "nubes_kafka_user"
},
{
"question": "Как развернуть RabbitMQ для очередей задач?",
"expected_pages": [
"rabbitmq"
],
"resource_type": "nubes_rabbitmq"
},
{
"question": "Как создать S3-бакет для хранения файлов?",
"expected_pages": [
"s3bucket",
"s3"
],
"resource_type": "nubes_s3bucket"
},
{
"question": "Как создать S3-хранилище и сублюзеров с ограниченными ключами?",
"expected_pages": [
"s3"
],
"resource_type": "nubes_s3_sub_user"
},
{
"question": "Как развернуть Kubernetes-кластер в Shturval?",
"expected_pages": [
"k8s_sthutrval_cluster"
],
"resource_type": "nubes_k8s_sthutrval_cluster"
},
{
"question": "Как установить Velero для бэкапа Kubernetes?",
"expected_pages": [
"k8s_velero"
],
"resource_type": "nubes_k8s_velero"
},
{
"question": "Как развернуть OpenBao для хранения секретов в кластере?",
"expected_pages": [
"k8s_openbao"
],
"resource_type": "nubes_k8s_openbao"
},
{
"question": "Как создать DNS-запись для своего домена?",
"expected_pages": [
"dnsrecord",
"zones_v2"
],
"resource_type": "nubes_dnsrecord"
},
{
"question": "Как создать организацию и vDC в VMware-облаке?",
"expected_pages": [
"vc_org",
"vc_vdc"
],
"resource_type": "nubes_vc_vdc"
},
{
"question": "Как настроить SNAT в NSX-T?",
"expected_pages": [
"vc_nsxt"
],
"resource_type": "nubes_vc_nsxt_snat"
},
{
"question": "Как развернуть LLM-модель с секретами и командой?",
"expected_pages": [
"llm_ai"
],
"resource_type": "nubes_llm_ai"
},
{
"question": "Как развернуть Nextcloud и настроить бэкап?",
"expected_pages": [
"nextcloud"
],
"resource_type": "nubes_nextcloud"
},
{
"question": "Как развернуть Node.js приложение и сделать передеплой?",
"expected_pages": [
"nodejs"
],
"resource_type": "nubes_nodejs"
},
{
"question": "Как задеплоить Flask-приложение и получить его URL?",
"expected_pages": [
"flask"
],
"resource_type": "nubes_flask"
},
{
"question": "Как развернуть pgAdmin для управления базами?",
"expected_pages": [
"pgadmin"
],
"resource_type": "nubes_pgadmin"
},
{
"question": "Как подключиться к API Terraform-провайдера Nubes?",
"expected_pages": [],
"resource_type": ""
},
{
"question": "Какие версии провайдера сейчас доступны и как их обновлять?",
"expected_pages": [
"template"
],
"resource_type": ""
},
{
"question": "Как создать nubes_instance с плавающим IP floating_ip?",
"expected_pages": [],
"resource_type": ""
}
] ]