140 lines
4.5 KiB
JavaScript
140 lines
4.5 KiB
JavaScript
/**
|
|
* Lightweight local RAG for QVAC — no native embeddings required.
|
|
* Keyword retrieval over guide snippets + live chart catalog.
|
|
*/
|
|
|
|
/** Built-in operator knowledge (subset of user-guide). */
|
|
const GUIDE_DOCS = [
|
|
{
|
|
id: 'guide:connect',
|
|
title: 'Connect',
|
|
text: 'Connect to a PearData agent with public key or pd1 invite. Viewer vs operator vs admin roles. Saved bookmarks restore agents. Active peer is used by all tools.',
|
|
},
|
|
{
|
|
id: 'guide:charts',
|
|
title: 'Charts',
|
|
text: 'Metrics wall with sections TOC, play pause, time presets 1m 5m 15m 1h 6h, pin board, correlate brush, related metrics, chart types line area stacked bar pie, dimension legend.',
|
|
},
|
|
{
|
|
id: 'guide:alerts',
|
|
title: 'Alerts',
|
|
text: 'Anomaly list with severity warning critical, Show jumps to chart, Correlate opens metric correlations around event time, silence TTL re-enables.',
|
|
},
|
|
{
|
|
id: 'guide:processes',
|
|
title: 'Processes',
|
|
text: 'Live process table from /proc when PEARDATA_PROCESSES=1, sort by cpu rss, tree view, CSV export, open top process charts.',
|
|
},
|
|
{
|
|
id: 'guide:logs',
|
|
title: 'Logs',
|
|
text: 'System logs journal default, anomalies source, audit admin, follow stream, search unit priority.',
|
|
},
|
|
{
|
|
id: 'guide:retention',
|
|
title: 'Data retention',
|
|
text: 'Settings Data Manager hot tier0 warm tier1 HyperDB prune age disk budget. Longer chart windows may be sparse until samples accumulate.',
|
|
},
|
|
{
|
|
id: 'guide:qvac',
|
|
title: 'QVAC',
|
|
text: 'Local AI copilot on desktop. Tools call agent RPC never invent metrics. Profiles lite recommended strong. Install @qvac/sdk for full LLM else tools-only fallback.',
|
|
},
|
|
{
|
|
id: 'guide:fleet',
|
|
title: 'Fleet',
|
|
text: 'Multi-host roster set active reconnect forget. Parent collector aggregates child getHealth. getFleetHealth RPC.',
|
|
},
|
|
{
|
|
id: 'guide:kpis',
|
|
title: 'Core KPIs',
|
|
text: 'system.cpu user system idle iowait, system.ram used, system.load load1, system.net received sent, system.io reads writes, mem.available.',
|
|
},
|
|
]
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @returns {string[]}
|
|
*/
|
|
function tokens(text) {
|
|
return String(text || '')
|
|
.toLowerCase()
|
|
.split(/[^a-z0-9._-]+/)
|
|
.filter((t) => t.length > 1)
|
|
}
|
|
|
|
/**
|
|
* Build docs from chart catalog map.
|
|
* @param {Record<string, object>} catalog
|
|
* @param {number} [cap]
|
|
*/
|
|
export function catalogDocs(catalog, cap = 400) {
|
|
/** @type {Array<{ id: string, title: string, text: string }>} */
|
|
const docs = []
|
|
for (const [id, meta] of Object.entries(catalog || {})) {
|
|
if (docs.length >= cap) break
|
|
const dims = Array.isArray(meta.dimensions)
|
|
? meta.dimensions
|
|
.map((d) => (typeof d === 'string' ? d : d?.id || d?.name || ''))
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
: ''
|
|
docs.push({
|
|
id: `chart:${id}`,
|
|
title: meta.title || id,
|
|
text: [id, meta.title, meta.context, meta.family, meta.plugin, meta.units, dims]
|
|
.filter(Boolean)
|
|
.join(' '),
|
|
})
|
|
}
|
|
return docs
|
|
}
|
|
|
|
/**
|
|
* @param {string} query
|
|
* @param {Array<{ id: string, title: string, text: string }>} docs
|
|
* @param {number} [topK]
|
|
*/
|
|
export function retrieve(query, docs, topK = 6) {
|
|
const qToks = tokens(query)
|
|
if (!qToks.length || !docs?.length) return []
|
|
/** @type {Array<{ id: string, title: string, text: string, score: number }>} */
|
|
const scored = []
|
|
for (const d of docs) {
|
|
const hay = tokens(`${d.title} ${d.text}`)
|
|
const set = new Set(hay)
|
|
let score = 0
|
|
for (const t of qToks) {
|
|
if (set.has(t)) score += 2
|
|
else if (hay.some((h) => h.includes(t) || t.includes(h))) score += 1
|
|
}
|
|
if (d.id.startsWith('chart:') && qToks.some((t) => d.id.includes(t))) score += 3
|
|
if (score > 0) scored.push({ ...d, score })
|
|
}
|
|
scored.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
return scored.slice(0, topK)
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* query: string,
|
|
* catalog?: Record<string, object>,
|
|
* topK?: number,
|
|
* includeGuide?: boolean,
|
|
* }} opts
|
|
*/
|
|
export function buildRagContext(opts) {
|
|
const docs = []
|
|
if (opts.includeGuide !== false) docs.push(...GUIDE_DOCS)
|
|
if (opts.catalog) docs.push(...catalogDocs(opts.catalog))
|
|
const hits = retrieve(opts.query, docs, opts.topK ?? 6)
|
|
if (!hits.length) return ''
|
|
const lines = hits.map((h) => {
|
|
const snippet = h.text.length > 180 ? h.text.slice(0, 177) + '…' : h.text
|
|
return `- [${h.id}] ${h.title}: ${snippet}`
|
|
})
|
|
return `Relevant local knowledge (do not invent beyond tools + this context):\n${lines.join('\n')}`
|
|
}
|
|
|
|
export { GUIDE_DOCS }
|