docs: 3 стенда (test/dev/prod), приоритет curated-примеров, инцидент генератора nav
This commit is contained in:
@@ -18,6 +18,22 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1.1 Стенды
|
||||||
|
|
||||||
|
Ассистент **stand-aware**: `stand` берётся из первого сегмента пути, индекс —
|
||||||
|
из `VM_DOCS_BASE_URL/<stand>/search/search_index.json`. Включён на всех трёх стендах:
|
||||||
|
|
||||||
|
| Стенд | URL | Виджет | Эндпоинт |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `nubes-test` | `/nubes-test/` | ✅ | ✅ |
|
||||||
|
| `nubes-dev` | `/nubes-dev/` | ✅ | ✅ |
|
||||||
|
| `nubes` (prod) | `/nubes/` | ✅ | ✅ |
|
||||||
|
|
||||||
|
Каждый стенд отвечает по **своему** индексу. Кэш индекса/ответов и лимиты
|
||||||
|
ключуются по стенду.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2. Архитектура
|
## 2. Архитектура
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -154,8 +170,11 @@ tf_docs (Node.js, кластер nodejsk8s) [orchestrator.js]
|
|||||||
отбрасывание стоп-слов (рус. + англ.).
|
отбрасывание стоп-слов (рус. + англ.).
|
||||||
- **Транслит** (`TRANSLIT` в `search.js`): «постгрес»→`postgres`, «бакет»→`bucket`,
|
- **Транслит** (`TRANSLIT` в `search.js`): «постгрес»→`postgres`, «бакет»→`bucket`,
|
||||||
«топик»→`topic` и т.д.
|
«топик»→`topic` и т.д.
|
||||||
- **Буст по типу страницы**: страница ресурса ×2.0, руководство (`_manual`) ×0.6,
|
- **Буст по типу страницы** (приоритет — «примеры в первую очередь»):
|
||||||
служебная (`curated`/`30_registry`) ×0.4.
|
- `curated/*` (готовые примеры, проверенные на реальных деплоях) — **×2.4**;
|
||||||
|
- страница ресурса — ×2.0;
|
||||||
|
- руководство (`_manual`) — ×0.6;
|
||||||
|
- служебная (`30_registry`) — ×0.4.
|
||||||
- **Подресурс → родитель**: `nubes_postgres_database` при поиске дополняется
|
- **Подресурс → родитель**: `nubes_postgres_database` при поиске дополняется
|
||||||
токеном `postgres` (`resourceBaseSegment`).
|
токеном `postgres` (`resourceBaseSegment`).
|
||||||
- **Предтокенизация**: токены всех 823 документов считаются один раз при загрузке
|
- **Предтокенизация**: токены всех 823 документов считаются один раз при загрузке
|
||||||
@@ -283,3 +302,26 @@ tf_docs (Node.js, кластер nodejsk8s) [orchestrator.js]
|
|||||||
6. Один под (replicas=1) — лимиты/кэш не шарятся между репликами (принято в брифе 7.4).
|
6. Один под (replicas=1) — лимиты/кэш не шарятся между репликами (принято в брифе 7.4).
|
||||||
7. Задержка доминируется LLM; ускоряется стримингом (SSE) или параллелизацией
|
7. Задержка доминируется LLM; ускоряется стримингом (SSE) или параллелизацией
|
||||||
этапа 1 с поиском (v2).
|
этапа 1 с поиском (v2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Исправленный инцидент: сборка `nubes-dev`
|
||||||
|
|
||||||
|
При публикации dev-документации сборка падала на YAML-ошибке nav:
|
||||||
|
|
||||||
|
```
|
||||||
|
- [DEPRECATED] Публичные IP адреса: ← невалидный YAML
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина:** генератор `TOOLS/docs-generator/internal/writers/writers.go`
|
||||||
|
(`WriteNavFragment`) писал `ServiceDisplayName` без кавычек. У `vcexternalip` в
|
||||||
|
dev имя = `[DEPRECATED] Публичные IP адреса` → ломал парсер.
|
||||||
|
|
||||||
|
**Фикс (корень):** в генератор добавлен `yamlScalar()` — заголовки с спецсимволами
|
||||||
|
(`[ ] : # & *` и др.) кавычатся. Затронул все стенды на будущее.
|
||||||
|
|
||||||
|
**Разовый обход:** для немедленной публикации dev строка в
|
||||||
|
`generated/dev/docs/_nav_fragment.yml` закавычена вручную (файл генерируемый,
|
||||||
|
перезапишется корректно после фикса генератора).
|
||||||
|
|
||||||
|
Коммит фикса: `tf_provider` `322235c`.
|
||||||
|
|||||||
@@ -19,91 +19,91 @@ const BASE_URL = 'https://api.aillm.ru';
|
|||||||
const MODEL = 'gpt-oss-120b';
|
const MODEL = 'gpt-oss-120b';
|
||||||
|
|
||||||
const QUESTIONS = [
|
const QUESTIONS = [
|
||||||
'openbao для секретов в кластере',
|
'openbao для секретов в кластере',
|
||||||
'как создать виртуальную машину',
|
'как создать виртуальную машину',
|
||||||
'как развернуть rabbitmq',
|
'как развернуть rabbitmq',
|
||||||
'nodejs приложение задеплоить',
|
'nodejs приложение задеплоить',
|
||||||
'как развернуть gitea',
|
'как развернуть gitea',
|
||||||
'pgadmin для управления базами',
|
'pgadmin для управления базами',
|
||||||
];
|
];
|
||||||
|
|
||||||
const REQUIRED_HEADERS = ['Роль', 'Задача', 'Контекст', 'Требования', 'Ограничения'];
|
const REQUIRED_HEADERS = ['Роль', 'Задача', 'Контекст', 'Требования', 'Ограничения'];
|
||||||
|
|
||||||
const STAGE2_SYSTEM_PROMPT =
|
const STAGE2_SYSTEM_PROMPT =
|
||||||
'Ты — ассистент документации Terraform-провайдера Nubes. ' +
|
'Ты — ассистент документации Terraform-провайдера Nubes. ' +
|
||||||
'Составь ответ СТРОГО по шаблону с заголовками-маркерами, каждый на новой строке:\n' +
|
'Составь ответ СТРОГО по шаблону с заголовками-маркерами, каждый на новой строке:\n' +
|
||||||
'Роль:\nЗадача:\nКонтекст:\nТребования:\nОграничения:\n' +
|
'Роль:\nЗадача:\nКонтекст:\nТребования:\nОграничения:\n' +
|
||||||
'Используй ТОЛЬКО факты из блока ДОКУМЕНТАЦИЯ ниже. ' +
|
'Используй ТОЛЬКО факты из блока ДОКУМЕНТАЦИЯ ниже. ' +
|
||||||
'Не выдумывай ресурсов, атрибутов и значений, которых нет в документации. ' +
|
'Не выдумывай ресурсов, атрибутов и значений, которых нет в документации. ' +
|
||||||
'Отвечай на русском языке.';
|
'Отвечай на русском языке.';
|
||||||
|
|
||||||
function findApiKey() {
|
function findApiKey() {
|
||||||
const lines = fs.readFileSync(KEY_FILE, 'utf8').split(/\r?\n/);
|
const lines = fs.readFileSync(KEY_FILE, 'utf8').split(/\r?\n/);
|
||||||
for (const l of lines) {
|
for (const l of lines) {
|
||||||
const t = l.trim();
|
const t = l.trim();
|
||||||
if (t.startsWith('sk-')) return t;
|
if (t.startsWith('sk-')) return t;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function httpGet(url) {
|
function httpGet(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const mod = url.startsWith('https') ? https : http;
|
const mod = url.startsWith('https') ? https : http;
|
||||||
const req = mod.get(url, (res) => {
|
const req = mod.get(url, (res) => {
|
||||||
if (res.statusCode !== 200) { res.resume(); reject(new Error('status ' + res.statusCode)); return; }
|
if (res.statusCode !== 200) { res.resume(); reject(new Error('status ' + res.statusCode)); return; }
|
||||||
let d = '';
|
let d = '';
|
||||||
res.on('data', (c) => { d += c; });
|
res.on('data', (c) => { d += c; });
|
||||||
res.on('end', () => resolve(d));
|
res.on('end', () => resolve(d));
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.setTimeout(15000, () => req.destroy(new Error('timeout')));
|
||||||
});
|
});
|
||||||
req.on('error', reject);
|
|
||||||
req.setTimeout(15000, () => req.destroy(new Error('timeout')));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function llmRaw(messages, maxTokens, apiKey) {
|
async function llmRaw(messages, maxTokens, apiKey) {
|
||||||
const resp = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
const resp = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ model: MODEL, messages, max_tokens: maxTokens, temperature: 0.2 }),
|
body: JSON.stringify({ model: MODEL, messages, max_tokens: maxTokens, temperature: 0.2 }),
|
||||||
signal: AbortSignal.timeout(60000),
|
signal: AbortSignal.timeout(60000),
|
||||||
});
|
});
|
||||||
const j = await resp.json();
|
const j = await resp.json();
|
||||||
const msg = j && j.choices && j.choices[0] && j.choices[0].message;
|
const msg = j && j.choices && j.choices[0] && j.choices[0].message;
|
||||||
return { content: msg ? msg.content : null, finish: j.choices && j.choices[0] ? j.choices[0].finish_reason : null };
|
return { content: msg ? msg.content : null, finish: j.choices && j.choices[0] ? j.choices[0].finish_reason : null };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const { loadIndex } = require('../../search.js');
|
const { loadIndex } = require('../../search.js');
|
||||||
const { mergeSearchResults, buildContext } = require('../../stage2.js');
|
const { mergeSearchResults, buildContext } = require('../../stage2.js');
|
||||||
const { stage1ExtractResourceType } = require('../../stage1.js');
|
const { stage1ExtractResourceType } = require('../../stage1.js');
|
||||||
|
|
||||||
const apiKey = findApiKey();
|
const apiKey = findApiKey();
|
||||||
const raw = await httpGet(`${VM}/${STAND}/search/search_index.json`);
|
const raw = await httpGet(`${VM}/${STAND}/search/search_index.json`);
|
||||||
const idx = loadIndex(raw);
|
const idx = loadIndex(raw);
|
||||||
const types = idx.listResourceTypes();
|
const types = idx.listResourceTypes();
|
||||||
console.log('Индекс docs:', idx.docs.length, '| ресурсов:', types.size, '\n');
|
console.log('Индекс docs:', idx.docs.length, '| ресурсов:', types.size, '\n');
|
||||||
|
|
||||||
for (const q of QUESTIONS) {
|
for (const q of QUESTIONS) {
|
||||||
console.log('='.repeat(80));
|
console.log('='.repeat(80));
|
||||||
console.log('ВОПРОС:', q);
|
console.log('ВОПРОС:', q);
|
||||||
const r1 = await stage1ExtractResourceType(q, { resourceTypesSet: types, baseUrl: BASE_URL, apiKey, model: MODEL });
|
const r1 = await stage1ExtractResourceType(q, { resourceTypesSet: types, baseUrl: BASE_URL, apiKey, model: MODEL });
|
||||||
const rt = r1.ok ? r1.resourceType : null;
|
const rt = r1.ok ? r1.resourceType : null;
|
||||||
console.log('этап1:', JSON.stringify(r1));
|
console.log('этап1:', JSON.stringify(r1));
|
||||||
const merged = mergeSearchResults(idx, q, rt, { topK: 5 });
|
const merged = mergeSearchResults(idx, q, rt, { topK: 5 });
|
||||||
console.log('контекст:', merged.map((m) => m.location).join(' , '));
|
console.log('контекст:', merged.map((m) => m.location).join(' , '));
|
||||||
const ctx = buildContext(merged, 1500);
|
const ctx = buildContext(merged, 1500);
|
||||||
const docsBlock = ctx.map((c, i) => `[${i + 1}] ${c.title}\n${c.text}`).join('\n\n');
|
const docsBlock = ctx.map((c, i) => `[${i + 1}] ${c.title}\n${c.text}`).join('\n\n');
|
||||||
const userContent = 'ДОКУМЕНТАЦИЯ:\n' + (docsBlock || '(нет релевантных фрагментов)') + '\n\nВОПРОС ПОЛЬЗОВАТЕЛЯ:\n' + q;
|
const userContent = 'ДОКУМЕНТАЦИЯ:\n' + (docsBlock || '(нет релевантных фрагментов)') + '\n\nВОПРОС ПОЛЬЗОВАТЕЛЯ:\n' + q;
|
||||||
const messages = [{ role: 'system', content: STAGE2_SYSTEM_PROMPT }, { role: 'user', content: userContent }];
|
const messages = [{ role: 'system', content: STAGE2_SYSTEM_PROMPT }, { role: 'user', content: userContent }];
|
||||||
|
|
||||||
const out = await llmRaw(messages, 1024, apiKey);
|
const out = await llmRaw(messages, 1024, apiKey);
|
||||||
console.log('finish_reason:', out.finish, '| content len:', out.content ? out.content.length : 0);
|
console.log('finish_reason:', out.finish, '| content len:', out.content ? out.content.length : 0);
|
||||||
const missing = out.content ? REQUIRED_HEADERS.filter((h) => !out.content.includes(h + ':')) : REQUIRED_HEADERS;
|
const missing = out.content ? REQUIRED_HEADERS.filter((h) => !out.content.includes(h + ':')) : REQUIRED_HEADERS;
|
||||||
console.log('НЕТ заголовков:', missing.join(', ') || '(нет, всё ок)');
|
console.log('НЕТ заголовков:', missing.join(', ') || '(нет, всё ок)');
|
||||||
console.log('--- СЫРОЙ ОТВЕТ (первые 500) ---');
|
console.log('--- СЫРОЙ ОТВЕТ (первые 500) ---');
|
||||||
console.log((out.content || '(null)').slice(0, 500));
|
console.log((out.content || '(null)').slice(0, 500));
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
|
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
|
||||||
|
|||||||
@@ -51,15 +51,15 @@ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.log('Инстанс:', UID, '| операция:', ACTION);
|
console.log('Инстанс:', UID, '| операция:', ACTION);
|
||||||
|
|
||||||
// 1) Состояние инстанса: доступные операции + текущие params.
|
// 1) Состояние инстанса: доступные операции + текущие params.
|
||||||
const st = await api('GET', `/instances/${UID}`);
|
const st = await api('GET', `/instances/${UID}`);
|
||||||
if (st.status !== 200) {
|
if (st.status !== 200) {
|
||||||
console.error('Ошибка GET /instances:', st.status, st.text.slice(0, 300));
|
console.error('Ошибка GET /instances:', st.status, st.text.slice(0, 300));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const inst = (st.json && st.json.instance) || {};
|
const inst = (st.json && st.json.instance) || {};
|
||||||
console.log('Статус:', inst.explainedStatus, '| inProgress:', inst.operationIsInProgress, '| pending:', inst.operationIsPending);
|
console.log('Статус:', inst.explainedStatus, '| inProgress:', inst.operationIsInProgress, '| pending:', inst.operationIsPending);
|
||||||
const instanceParams = (inst.state && inst.state.params) || {};
|
const instanceParams = (inst.state && inst.state.params) || {};
|
||||||
|
|
||||||
const ops = inst.availableOperations || [];
|
const ops = inst.availableOperations || [];
|
||||||
const op = ops.find((o) => String(o.operation).toLowerCase() === ACTION.toLowerCase());
|
const op = ops.find((o) => String(o.operation).toLowerCase() === ACTION.toLowerCase());
|
||||||
@@ -81,28 +81,28 @@ async function main() {
|
|||||||
}
|
}
|
||||||
if (!opUid) opUid = uidFromLocation(created.location);
|
if (!opUid) opUid = uidFromLocation(created.location);
|
||||||
if (!opUid) opUid = created.text.replace(/^"|"$/g, '').trim();
|
if (!opUid) opUid = created.text.replace(/^"|"$/g, '').trim();
|
||||||
console.log('Создана операция:', created.status, '| opUid:', opUid);
|
console.log('Создана операция:', created.status, '| opUid:', opUid);
|
||||||
if (!opUid || opUid.length < 8) {
|
if (!opUid || opUid.length < 8) {
|
||||||
console.error('Не удалось получить opUid. Ответ:', created.text.slice(0, 300));
|
console.error('Не удалось получить opUid. Ответ:', created.text.slice(0, 300));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
|
||||||
|
|
||||||
// 2б) Установить требуемые CFS-параметры (например startupConfiguration) из live-состояния.
|
|
||||||
const cfs = await api('GET', `/instanceOperations/${opUid}?fields=cfsParams`);
|
|
||||||
const cfsParams = (cfs.json && cfs.json.instanceOperation && cfs.json.instanceOperation.cfsParams) || [];
|
|
||||||
for (const p of cfsParams) {
|
|
||||||
const val = instanceParams[p.svcOperationCfsParam];
|
|
||||||
if (val === undefined) {
|
|
||||||
console.log(' параметр', p.svcOperationCfsParam, '(id=' + p.svcOperationCfsParamId + ') — в состоянии нет, пропуск');
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
const paramValue = typeof val === 'string' ? val : JSON.stringify(val);
|
|
||||||
const pr = await api('POST', '/instanceOperationCfsParams', {
|
// 2б) Установить требуемые CFS-параметры (например startupConfiguration) из live-состояния.
|
||||||
instanceOperationUid: opUid,
|
const cfs = await api('GET', `/instanceOperations/${opUid}?fields=cfsParams`);
|
||||||
svcOperationCfsParamId: p.svcOperationCfsParamId,
|
const cfsParams = (cfs.json && cfs.json.instanceOperation && cfs.json.instanceOperation.cfsParams) || [];
|
||||||
paramValue: paramValue,
|
for (const p of cfsParams) {
|
||||||
});
|
const val = instanceParams[p.svcOperationCfsParam];
|
||||||
console.log(' параметр', p.svcOperationCfsParam, '=', paramValue, '->', pr.status);
|
if (val === undefined) {
|
||||||
|
console.log(' параметр', p.svcOperationCfsParam, '(id=' + p.svcOperationCfsParamId + ') — в состоянии нет, пропуск');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const paramValue = typeof val === 'string' ? val : JSON.stringify(val);
|
||||||
|
const pr = await api('POST', '/instanceOperationCfsParams', {
|
||||||
|
instanceOperationUid: opUid,
|
||||||
|
svcOperationCfsParamId: p.svcOperationCfsParamId,
|
||||||
|
paramValue: paramValue,
|
||||||
|
});
|
||||||
|
console.log(' параметр', p.svcOperationCfsParam, '=', paramValue, '->', pr.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Запустить операцию.
|
// 3) Запустить операцию.
|
||||||
|
|||||||
Reference in New Issue
Block a user