231 lines
8.4 KiB
Markdown
231 lines
8.4 KiB
Markdown
# QVAC integration (local AI)
|
||
|
||
PearData’s **QVAC** tab is a local-first SRE copilot. Inference runs on the **desktop** (Electron / Pear); the agent only serves metrics over existing P2P RPC / REST. No cloud LLM is required.
|
||
|
||
Upstream: [tetherto/qvac](https://github.com/tetherto/qvac) · [docs.qvac.tether.io](https://docs.qvac.tether.io) · [Electron packaging tutorial](https://docs.qvac.tether.io/tutorials/electron)
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Desktop QVAC tab
|
||
→ ui/qvac (engine + tools + onboarding)
|
||
→ @qvac/sdk (+ Bare worker) OR tools-only fallback
|
||
→ manager.request(Methods.*)
|
||
→ PearMonitor agent
|
||
```
|
||
|
||
| Layer | Responsibility |
|
||
|-------|----------------|
|
||
| Desktop | Model download/load, chat UI, tool loop, Bare worker |
|
||
| Agent | Lean composite reads (`getHostSnapshot`, `searchCharts`, `summarizeChart`) + existing metrics RPCs |
|
||
| REST | `/api/v3/ai/*` mirrors for scripts |
|
||
|
||
Agent overhead is unchanged when the QVAC tab is unused (no model load on the agent).
|
||
|
||
## Why `pear run` worked but CI releases did not
|
||
|
||
| Environment | How `@qvac/sdk` was resolved |
|
||
|-------------|------------------------------|
|
||
| **`pear run -d .` on a dev machine** | Often found a **hoisted** install (e.g. `~/node_modules/@qvac/sdk`) even when the project did not declare the dependency |
|
||
| **CI Electron client** | Clean `npm ci` in the repo — **no `@qvac/sdk`**, no Bare worker entry, asar packing blocked natives |
|
||
|
||
CI / rolling releases only ship what is in **this** package’s `dependencies` + forge output. Dev must not rely on parent `node_modules`.
|
||
|
||
## Packaging (Electron / CI)
|
||
|
||
### Dependencies
|
||
|
||
```json
|
||
"dependencies": {
|
||
"@qvac/sdk": "^0.16.0"
|
||
},
|
||
"devDependencies": {
|
||
"@electron-forge/plugin-base": "^7.11.2"
|
||
}
|
||
```
|
||
|
||
`@electron-forge/plugin-base` is the peer for `@qvac/sdk/electron-forge`.
|
||
|
||
### Config
|
||
|
||
`qvac.config.json` lists **only** the plugins PearData needs (keeps the worker smaller):
|
||
|
||
```json
|
||
{
|
||
"plugins": ["@qvac/sdk/llamacpp-completion/plugin"]
|
||
}
|
||
```
|
||
|
||
### Forge plugin
|
||
|
||
`forge.config.cjs` loads `QvacForgePlugin` when the SDK is installed. On package it:
|
||
|
||
1. Runs `bundleSdk` + `verifyBundle` for the target `platform-arch`
|
||
2. Forces **`asar: false`** (Bare cannot load `.bare` addons from asar)
|
||
3. Tree-shakes unused `@qvac/*` addons and prunes foreign prebuilds
|
||
|
||
Official caveats: [Electron tutorial → packaging](https://docs.qvac.tether.io/tutorials/electron#step-4-package-for-distribution).
|
||
|
||
### Escape hatch
|
||
|
||
```bash
|
||
PEARDATA_SKIP_QVAC=1 npm run make:client:linux-x64
|
||
```
|
||
|
||
Produces a **tools-only** client (no LLM natives). QVAC tab still works via RPC tools.
|
||
|
||
### Bare runtime binary
|
||
|
||
QVAC spawns a **Bare** worker via `bare-runtime` + a platform package such as `bare-runtime-darwin-arm64`.
|
||
|
||
These are listed under **optionalDependencies** (os/cpu-filtered). On a Linux CI runner
|
||
`npm ci` will **not** install the darwin/win32 packages, so packaging must force-install them
|
||
first (see `scripts/ensure-qvac-bare-runtimes.cjs` and [CI.md](./CI.md)).
|
||
|
||
Forge keeps `bare-runtime` + `bare-runtime-<targetHost>` in the app and **fails the build**
|
||
if the binary is missing when QVAC packaging is enabled.
|
||
|
||
If you still see:
|
||
|
||
```text
|
||
Could not load the Bare runtime binary for darwin-arm64
|
||
… bare-runtime-darwin-arm64 … missing
|
||
```
|
||
|
||
on a local or CI client:
|
||
|
||
```bash
|
||
npm ci
|
||
npm run ensure:qvac-bare-runtimes # force-install all QVAC host binaries
|
||
npm run make:client:darwin-arm64
|
||
```
|
||
|
||
### Worker RPC timeout (`RPC initialization timed out after 30000ms`)
|
||
|
||
`bundleSdk` writes **absolute** `file:///build-machine/...` imports into
|
||
`qvac/worker.entry.mjs`. Off the build host Bare cannot load them, so the worker
|
||
never handshakes.
|
||
|
||
Fix (already wired into forge + `build-qvac-worker`):
|
||
|
||
```bash
|
||
node scripts/rewrite-qvac-worker-entry.cjs --force-portable
|
||
```
|
||
|
||
That rewrites the entry to **relative** `../node_modules/@qvac/sdk/...` imports
|
||
so the packaged app is self-contained.
|
||
|
||
### Native host matrix (LLM prebuilds)
|
||
|
||
`@qvac/llm-llamacpp` ships Bare prebuilds for:
|
||
|
||
| Host | Full QVAC | Notes |
|
||
|------|-----------|--------|
|
||
| `linux-x64` | yes | |
|
||
| `linux-arm64` | yes | |
|
||
| `darwin-x64` | yes | CPU-oriented |
|
||
| `darwin-arm64` | yes | Metal |
|
||
| `win32-x64` | yes | Vulkan / CPU |
|
||
| `win32-arm64` | **no** | No published prebuild — CI packages **tools-only** automatically |
|
||
|
||
`scripts/make.cjs` and `forge.config.cjs` set `PEARDATA_SKIP_QVAC=1` for unsupported hosts so the release matrix still emits a client zip.
|
||
|
||
### Local parity with CI
|
||
|
||
```bash
|
||
npm ci # installs @qvac/sdk into THIS project
|
||
npm run build:client-bundle
|
||
npm run build:qvac-worker # optional local bare-pack
|
||
npm run make:client:darwin-arm64 # or any host from scripts/hosts.cjs
|
||
```
|
||
|
||
Increase client package timeout if needed: `PEARDATA_CLIENT_TIMEOUT_MS=1800000`.
|
||
|
||
### Runtime paths (packaged Electron)
|
||
|
||
`electron/main.cjs` sets `QVAC_WORKER_PATH` to the first existing:
|
||
|
||
- `app/qvac/worker.entry.mjs`
|
||
- `app/qvac/worker.bundle.js`
|
||
- `resources/qvac/...` / asar.unpacked variants
|
||
|
||
SDK also searches those locations itself ([node-rpc-client](https://github.com/tetherto/qvac)).
|
||
|
||
## Pear desktop
|
||
|
||
| Piece | Role |
|
||
|-------|------|
|
||
| `qvac/worker.pear.entry.mjs` | Bare worker entry (LLM plugin only) |
|
||
| `package.json` → `pear.stage.entrypoints` | Stages the worker for pear |
|
||
| `pear.pre` | Stays `pear-electron/pre` (single pipe owner) |
|
||
|
||
Install deps in the **project** (`npm i` / `npm ci`), not only in a parent folder:
|
||
|
||
```bash
|
||
npm ci
|
||
pear run -d .
|
||
```
|
||
|
||
Optional combined pre (advanced): `scripts/pear-pre.mjs` — only if you understand pear-pipe ownership; default remains `pear-electron/pre`.
|
||
|
||
## Model profiles
|
||
|
||
| Profile | Chat constant | Tools | Typical use |
|
||
|---------|---------------|-------|-------------|
|
||
| Lite | `QWEN3_600M_INST_Q4` | limited | ≤8 GB RAM |
|
||
| **Recommended** | `QWEN3_1_7B_INST_Q4` | yes | Default |
|
||
| Strong | `QWEN3_4B_INST_Q4_K_M` | yes | ≥16 GB RAM |
|
||
| Tool-tiny | `LLAMA_TOOL_CALLING_1B_INST_Q4_K` | yes | Fallback |
|
||
|
||
Without the SDK (or if load fails), the tab stays in **tools-only** mode: live agent RPCs, no generative model.
|
||
|
||
## Agent RPCs (viewer)
|
||
|
||
| Method | Purpose |
|
||
|--------|---------|
|
||
| `getHostSnapshot` | Health + KPIs + anomalies/alerts + catalog/storage summary |
|
||
| `investigateHost` | One-shot diagnosis pack: findings, hot charts, top processes |
|
||
| `hotMetrics` | Charts with strongest recent change / anomaly signal |
|
||
| `relatedCharts` | Related charts for a seed id (catalog + weights) |
|
||
| `compareChartWindows` | Baseline vs highlight window comparison on one chart |
|
||
| `summarizeCharts` | Batch summarize up to 12 charts |
|
||
| `searchCharts` | `{ q, limit }` catalog search |
|
||
| `summarizeChart` | `{ chart, after?, points? }` min/avg/max/last per dim |
|
||
|
||
REST: `GET /api/v3/ai/snapshot`, `/api/v3/ai/investigate`, `/api/v3/ai/hot`, `/api/v3/ai/related`, `/api/v3/ai/compare`, `/api/v3/ai/summarize`, `/api/v3/ai/charts`, `/api/v3/ai/chart/:id/summary`.
|
||
|
||
## Tools the chat can call
|
||
|
||
Tools are **tiered** so small models stay within context:
|
||
|
||
| Tier | When | Tools |
|
||
|------|------|-------|
|
||
| **core** | All tool-enabled profiles | `investigate_host`, `host_snapshot`, `search_charts`, `summarize_chart`, `list_anomalies`, `list_alerts`, `list_processes`, `local_knowledge`, `open_chart`, `open_view` |
|
||
| **deep** | Recommended / Strong | `hot_metrics`, `related_charts`, `compare_chart_windows`, `summarize_charts`, `query_metric`, `get_weights`, `query_logs`, `fleet_health`, `list_child_peers`, `storage_info`, `agent_health`, `node_info`, `db_info`, `list_contexts`, `get_chart`, `list_jobs`, `get_alert` |
|
||
| **write** | Operator role + confirm | `silence_alert`, `ack_alert`, `run_job` |
|
||
|
||
On context overflow the engine first drops to **core** tools, then drops tool schemas entirely and retries.
|
||
|
||
## Settings
|
||
|
||
| Key | Default | Meaning |
|
||
|-----|---------|---------|
|
||
| `qvacOnboarded` | `false` | Setup wizard completed |
|
||
| `qvacProfile` | `recommended` | Model profile id |
|
||
| `qvacMode` | `''` | `qvac` \| `fallback` after last load |
|
||
| `qvacRag` | `true` | Inject guide + catalog snippets |
|
||
| `qvacIdleUnloadMin` | `30` | Unload LLM after idle minutes (`0` = never) |
|
||
|
||
## Related files
|
||
|
||
| Path | Role |
|
||
|------|------|
|
||
| `ui/qvac/*` | Desktop UI + engine |
|
||
| `qvac.config.json` | Plugin allow-list for bundling |
|
||
| `qvac/worker.entry.mjs` | Electron/Node worker entry |
|
||
| `qvac/worker.pear.entry.mjs` | Pear worker entry |
|
||
| `forge.config.cjs` | `QvacForgePlugin` + ignore rules |
|
||
| `scripts/build-qvac-worker.cjs` | Local `bundleSdk` |
|
||
| `server/services/ai-tools.js` | Agent composite reads |
|
||
| `user-guide/qvac.md` | Operator walkthrough |
|