/** * 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} 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, * 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 }