@vivantel/virage-cli 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/virage.js +24 -14
- package/dist/bin/virage.js.map +1 -1
- package/dist/cli/benchmark.d.ts +3 -2
- package/dist/cli/benchmark.d.ts.map +1 -1
- package/dist/cli/benchmark.js +72 -21
- package/dist/cli/benchmark.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +406 -171
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/dashboard-ui/assets/index-DAtIVwZG.css +1 -0
- package/dist/dashboard-ui/assets/index-DRAVxpLs.js +52 -0
- package/dist/dashboard-ui/index.html +2 -2
- package/package.json +7 -3
- package/dist/dashboard-ui/assets/index-Dwen7D60.js +0 -40
- package/dist/dashboard-ui/assets/index-sUxHp5o1.css +0 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
3
4
|
import { existsSync } from "fs";
|
|
4
|
-
import { join,
|
|
5
|
+
import { join, resolve, basename } from "path";
|
|
5
6
|
import { homedir } from "os";
|
|
6
7
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { EmbeddingsDb } from "@vivantel/virage-core";
|
|
8
|
-
const MIME = {
|
|
9
|
-
".html": "text/html; charset=utf-8",
|
|
10
|
-
".js": "application/javascript",
|
|
11
|
-
".css": "text/css",
|
|
12
|
-
".svg": "image/svg+xml",
|
|
13
|
-
".ico": "image/x-icon",
|
|
14
|
-
};
|
|
8
|
+
import { EmbeddingsDb, loadConfig, Orchestrator, ExperimentStore, bootstrapPairedTest, generateEvalDataset, EvalRunner, loadEvalDataset, } from "@vivantel/virage-core";
|
|
15
9
|
const RECENT_PROJECTS_PATH = join(homedir(), ".virage", "recent-projects.json");
|
|
16
10
|
const MAX_PROJECTS = 10;
|
|
17
11
|
let projectsState = { projects: [], activeIndex: 0 };
|
|
18
|
-
//
|
|
19
|
-
|
|
12
|
+
// Cache the loaded config per project root to avoid reloading the embedder model on every search
|
|
13
|
+
let configCache = null;
|
|
14
|
+
// Single-operation guard for WebSocket pipeline runs
|
|
15
|
+
let wsOperationRunning = false;
|
|
20
16
|
const THIS_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
21
17
|
const UI_DIR = join(THIS_DIR, "..", "dashboard-ui");
|
|
22
18
|
const HAS_UI = existsSync(join(UI_DIR, "index.html"));
|
|
@@ -37,7 +33,7 @@ async function loadRecentProjects() {
|
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
catch {
|
|
40
|
-
/* first run
|
|
36
|
+
/* first run */
|
|
41
37
|
}
|
|
42
38
|
return { projects: [], activeIndex: 0 };
|
|
43
39
|
}
|
|
@@ -51,166 +47,20 @@ async function saveRecentProjects(state) {
|
|
|
51
47
|
await mkdir(join(homedir(), ".virage"), { recursive: true });
|
|
52
48
|
await writeFile(RECENT_PROJECTS_PATH, JSON.stringify(next, null, 2), "utf-8");
|
|
53
49
|
projectsState = next;
|
|
50
|
+
configCache = null; // invalidate on project change
|
|
54
51
|
}
|
|
55
|
-
function
|
|
56
|
-
return
|
|
57
|
-
const chunks = [];
|
|
58
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
59
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
60
|
-
req.on("error", reject);
|
|
61
|
-
});
|
|
52
|
+
function activeProject() {
|
|
53
|
+
return projectsState.projects[projectsState.activeIndex];
|
|
62
54
|
}
|
|
63
|
-
async function
|
|
64
|
-
if (
|
|
65
|
-
return
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const content = await readFile(filePath);
|
|
72
|
-
res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
73
|
-
res.end(content);
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// Try index.html for SPA client-side routing (non-asset paths)
|
|
78
|
-
if (!normalized.startsWith("/assets/") && ext === ".html") {
|
|
79
|
-
try {
|
|
80
|
-
const html = await readFile(join(UI_DIR, "index.html"));
|
|
81
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
82
|
-
res.end(html);
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
export async function runDashboard(opts) {
|
|
93
|
-
if (!HAS_UI) {
|
|
94
|
-
console.warn("⚠️ Dashboard UI not found. Run `npm run build -w @vivantel/virage-dashboard` first.");
|
|
95
|
-
}
|
|
96
|
-
// Derive the startup project from CLI options and register it
|
|
97
|
-
const startupRoot = resolve(opts.dbPath, "..", "..");
|
|
98
|
-
const startupProject = projectFromRoot(startupRoot, {
|
|
99
|
-
embeddingsDb: resolve(opts.dbPath),
|
|
100
|
-
});
|
|
101
|
-
const loaded = await loadRecentProjects();
|
|
102
|
-
const existingIdx = loaded.projects.findIndex((p) => p.rootPath === startupProject.rootPath);
|
|
103
|
-
if (existingIdx >= 0) {
|
|
104
|
-
loaded.projects[existingIdx] = startupProject;
|
|
105
|
-
loaded.activeIndex = existingIdx;
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
loaded.projects.unshift(startupProject);
|
|
109
|
-
loaded.activeIndex = 0;
|
|
110
|
-
}
|
|
111
|
-
await saveRecentProjects(loaded);
|
|
112
|
-
const server = createServer(async (req, res) => {
|
|
113
|
-
const url = req.url ?? "/";
|
|
114
|
-
res.setHeader("Content-Type", "application/json");
|
|
115
|
-
try {
|
|
116
|
-
const active = projectsState.projects[projectsState.activeIndex];
|
|
117
|
-
if (url === "/api/status" && req.method === "GET") {
|
|
118
|
-
const status = await getStatus(active.embeddingsDb);
|
|
119
|
-
res.end(JSON.stringify(status));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (url === "/api/chunks" && req.method === "GET") {
|
|
123
|
-
const chunks = await getChunksHistogram(active.embeddingsDb);
|
|
124
|
-
res.end(JSON.stringify(chunks));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (url === "/api/embeddings/anomalies" && req.method === "GET") {
|
|
128
|
-
const anomalies = await getAnomalies(active.embeddingsDb);
|
|
129
|
-
res.end(JSON.stringify(anomalies));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (url === "/api/projects" && req.method === "GET") {
|
|
133
|
-
res.end(JSON.stringify(projectsState));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (url === "/api/projects/add" && req.method === "POST") {
|
|
137
|
-
const body = JSON.parse(await readBody(req));
|
|
138
|
-
if (typeof body.rootPath !== "string" || !body.rootPath.trim()) {
|
|
139
|
-
res.statusCode = 400;
|
|
140
|
-
res.end(JSON.stringify({ error: "rootPath is required" }));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const entry = projectFromRoot(body.rootPath.trim());
|
|
144
|
-
if (!existsSync(entry.embeddingsDb)) {
|
|
145
|
-
res.statusCode = 422;
|
|
146
|
-
res.end(JSON.stringify({
|
|
147
|
-
error: `No .virage data found in ${entry.rootPath}`,
|
|
148
|
-
}));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const idx = projectsState.projects.findIndex((p) => p.rootPath === entry.rootPath);
|
|
152
|
-
const next = {
|
|
153
|
-
...projectsState,
|
|
154
|
-
projects: [...projectsState.projects],
|
|
155
|
-
};
|
|
156
|
-
if (idx >= 0) {
|
|
157
|
-
next.projects[idx] = { ...entry, lastUsed: Date.now() };
|
|
158
|
-
next.activeIndex = idx;
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
next.projects = [entry, ...next.projects];
|
|
162
|
-
next.activeIndex = 0;
|
|
163
|
-
}
|
|
164
|
-
await saveRecentProjects(next);
|
|
165
|
-
res.end(JSON.stringify(projectsState));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (url === "/api/projects/switch" && req.method === "POST") {
|
|
169
|
-
const body = JSON.parse(await readBody(req));
|
|
170
|
-
const index = Number(body.index);
|
|
171
|
-
if (!Number.isInteger(index) ||
|
|
172
|
-
index < 0 ||
|
|
173
|
-
index >= projectsState.projects.length) {
|
|
174
|
-
res.statusCode = 400;
|
|
175
|
-
res.end(JSON.stringify({ error: "Invalid index" }));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const next = {
|
|
179
|
-
projects: [...projectsState.projects],
|
|
180
|
-
activeIndex: index,
|
|
181
|
-
};
|
|
182
|
-
next.projects[index] = {
|
|
183
|
-
...next.projects[index],
|
|
184
|
-
lastUsed: Date.now(),
|
|
185
|
-
};
|
|
186
|
-
await saveRecentProjects(next);
|
|
187
|
-
res.end(JSON.stringify(projectsState));
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
// Serve React app static assets
|
|
191
|
-
const served = await serveStatic(url, res);
|
|
192
|
-
if (!served) {
|
|
193
|
-
res.statusCode = 404;
|
|
194
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
res.statusCode = 500;
|
|
199
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
server.listen(opts.port, () => {
|
|
203
|
-
console.log(`🚀 RAG Dashboard running at http://localhost:${opts.port}`);
|
|
204
|
-
console.log(" Press Ctrl+C to stop");
|
|
205
|
-
});
|
|
206
|
-
// Keep process alive
|
|
207
|
-
await new Promise((resolve) => {
|
|
208
|
-
process.on("SIGINT", () => {
|
|
209
|
-
server.close();
|
|
210
|
-
resolve();
|
|
211
|
-
});
|
|
212
|
-
});
|
|
55
|
+
async function getCachedConfig(configPath) {
|
|
56
|
+
if (configCache?.path === configPath)
|
|
57
|
+
return configCache.cfg;
|
|
58
|
+
const cfg = await loadConfig(configPath);
|
|
59
|
+
await cfg.vectorStore.initialize();
|
|
60
|
+
configCache = { path: configPath, cfg };
|
|
61
|
+
return cfg;
|
|
213
62
|
}
|
|
63
|
+
// ─── Status helpers (preserved from original) ─────────────────────────────────
|
|
214
64
|
async function getStatus(dbPath) {
|
|
215
65
|
let totalChunks = 0;
|
|
216
66
|
let totalEmbeddings = 0;
|
|
@@ -273,4 +123,389 @@ async function getAnomalies(dbPath) {
|
|
|
273
123
|
return { anomalies: [] };
|
|
274
124
|
}
|
|
275
125
|
}
|
|
126
|
+
// ─── WebSocket handler ─────────────────────────────────────────────────────────
|
|
127
|
+
function safeSend(ws, msg) {
|
|
128
|
+
if (ws.readyState === ws.OPEN) {
|
|
129
|
+
ws.send(JSON.stringify(msg));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function handleWsOperation(ws, op) {
|
|
133
|
+
const active = activeProject();
|
|
134
|
+
if (!active) {
|
|
135
|
+
safeSend(ws, { type: "error", message: "No active project" });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const configPath = join(active.rootPath, "virage.config.json");
|
|
139
|
+
if (!existsSync(configPath)) {
|
|
140
|
+
safeSend(ws, {
|
|
141
|
+
type: "error",
|
|
142
|
+
message: `virage.config.json not found in ${active.rootPath}`,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const cfg = await getCachedConfig(configPath);
|
|
148
|
+
if (op === "update") {
|
|
149
|
+
const orchestrator = new Orchestrator({
|
|
150
|
+
...cfg,
|
|
151
|
+
options: {
|
|
152
|
+
...cfg.options,
|
|
153
|
+
onChunkProgress: (done, total) => safeSend(ws, { type: "progress", stage: "chunk", done, total }),
|
|
154
|
+
onEmbedProgress: (done, total) => safeSend(ws, { type: "progress", stage: "embed", done, total }),
|
|
155
|
+
onUploadProgress: (done, total) => safeSend(ws, { type: "progress", stage: "upload", done, total }),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
safeSend(ws, { type: "progress", stage: "starting", done: 0, total: 0 });
|
|
159
|
+
await orchestrator.run();
|
|
160
|
+
safeSend(ws, { type: "done" });
|
|
161
|
+
}
|
|
162
|
+
else if (op === "eval-generate") {
|
|
163
|
+
const db = new EmbeddingsDb(active.embeddingsDb);
|
|
164
|
+
const chunks = db.getAllChunks();
|
|
165
|
+
db.close();
|
|
166
|
+
safeSend(ws, {
|
|
167
|
+
type: "progress",
|
|
168
|
+
stage: "eval-generate",
|
|
169
|
+
message: `Generating eval dataset from ${chunks.length} chunks...`,
|
|
170
|
+
});
|
|
171
|
+
const outputPath = join(active.rootPath, ".virage", "eval-dataset.json");
|
|
172
|
+
await generateEvalDataset(chunks, { includeNegatives: false, paraphraseRatio: 0 }, outputPath);
|
|
173
|
+
safeSend(ws, {
|
|
174
|
+
type: "done",
|
|
175
|
+
message: `Eval dataset written to ${outputPath}`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else if (op === "evaluate") {
|
|
179
|
+
const datasetPath = join(active.rootPath, ".virage", "eval-dataset.json");
|
|
180
|
+
if (!existsSync(datasetPath)) {
|
|
181
|
+
safeSend(ws, {
|
|
182
|
+
type: "error",
|
|
183
|
+
message: "No eval dataset found. Run eval-generate first.",
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const dataset = await loadEvalDataset(datasetPath);
|
|
188
|
+
safeSend(ws, {
|
|
189
|
+
type: "progress",
|
|
190
|
+
stage: "evaluate",
|
|
191
|
+
message: `Evaluating ${dataset.queries.length} queries...`,
|
|
192
|
+
});
|
|
193
|
+
const runner = new EvalRunner(cfg.vectorStore, cfg.embedder, dataset, 10);
|
|
194
|
+
const result = await runner.run((completed, total) => safeSend(ws, {
|
|
195
|
+
type: "progress",
|
|
196
|
+
stage: "evaluate",
|
|
197
|
+
done: completed,
|
|
198
|
+
total,
|
|
199
|
+
}));
|
|
200
|
+
safeSend(ws, { type: "done", result });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
safeSend(ws, { type: "error", message: `Unknown operation: ${op}` });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
safeSend(ws, {
|
|
208
|
+
type: "error",
|
|
209
|
+
message: err instanceof Error ? err.message : String(err),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─── Main entry point ──────────────────────────────────────────────────────────
|
|
214
|
+
export async function runDashboard(opts) {
|
|
215
|
+
if (!HAS_UI) {
|
|
216
|
+
console.warn("⚠️ Dashboard UI not found. Run `npm run build -w @vivantel/virage-dashboard` first.");
|
|
217
|
+
}
|
|
218
|
+
const startupRoot = resolve(opts.dbPath, "..", "..");
|
|
219
|
+
const startupProject = projectFromRoot(startupRoot, {
|
|
220
|
+
embeddingsDb: resolve(opts.dbPath),
|
|
221
|
+
});
|
|
222
|
+
const loaded = await loadRecentProjects();
|
|
223
|
+
const existingIdx = loaded.projects.findIndex((p) => p.rootPath === startupProject.rootPath);
|
|
224
|
+
if (existingIdx >= 0) {
|
|
225
|
+
loaded.projects[existingIdx] = startupProject;
|
|
226
|
+
loaded.activeIndex = existingIdx;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
loaded.projects.unshift(startupProject);
|
|
230
|
+
loaded.activeIndex = 0;
|
|
231
|
+
}
|
|
232
|
+
await saveRecentProjects(loaded);
|
|
233
|
+
const app = express();
|
|
234
|
+
app.use(express.json());
|
|
235
|
+
// ─── Existing routes ────────────────────────────────────────────────────────
|
|
236
|
+
app.get("/api/status", async (_req, res) => {
|
|
237
|
+
const active = activeProject();
|
|
238
|
+
res.json(await getStatus(active?.embeddingsDb ?? opts.dbPath));
|
|
239
|
+
});
|
|
240
|
+
app.get("/api/chunks", async (_req, res) => {
|
|
241
|
+
const active = activeProject();
|
|
242
|
+
res.json(await getChunksHistogram(active?.embeddingsDb ?? opts.dbPath));
|
|
243
|
+
});
|
|
244
|
+
app.get("/api/embeddings/anomalies", async (_req, res) => {
|
|
245
|
+
const active = activeProject();
|
|
246
|
+
res.json(await getAnomalies(active?.embeddingsDb ?? opts.dbPath));
|
|
247
|
+
});
|
|
248
|
+
app.get("/api/projects", (_req, res) => {
|
|
249
|
+
res.json(projectsState);
|
|
250
|
+
});
|
|
251
|
+
app.post("/api/projects/add", async (req, res) => {
|
|
252
|
+
const body = req.body;
|
|
253
|
+
if (typeof body.rootPath !== "string" || !body.rootPath.trim()) {
|
|
254
|
+
res.status(400).json({ error: "rootPath is required" });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const entry = projectFromRoot(body.rootPath.trim());
|
|
258
|
+
if (!existsSync(entry.embeddingsDb)) {
|
|
259
|
+
res.status(422).json({
|
|
260
|
+
error: `No .virage data found in ${entry.rootPath}`,
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const idx = projectsState.projects.findIndex((p) => p.rootPath === entry.rootPath);
|
|
265
|
+
const next = { ...projectsState, projects: [...projectsState.projects] };
|
|
266
|
+
if (idx >= 0) {
|
|
267
|
+
next.projects[idx] = { ...entry, lastUsed: Date.now() };
|
|
268
|
+
next.activeIndex = idx;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
next.projects = [entry, ...next.projects];
|
|
272
|
+
next.activeIndex = 0;
|
|
273
|
+
}
|
|
274
|
+
await saveRecentProjects(next);
|
|
275
|
+
res.json(projectsState);
|
|
276
|
+
});
|
|
277
|
+
app.post("/api/projects/switch", async (req, res) => {
|
|
278
|
+
const body = req.body;
|
|
279
|
+
const index = Number(body.index);
|
|
280
|
+
if (!Number.isInteger(index) ||
|
|
281
|
+
index < 0 ||
|
|
282
|
+
index >= projectsState.projects.length) {
|
|
283
|
+
res.status(400).json({ error: "Invalid index" });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const next = {
|
|
287
|
+
projects: [...projectsState.projects],
|
|
288
|
+
activeIndex: index,
|
|
289
|
+
};
|
|
290
|
+
next.projects[index] = { ...next.projects[index], lastUsed: Date.now() };
|
|
291
|
+
await saveRecentProjects(next);
|
|
292
|
+
res.json(projectsState);
|
|
293
|
+
});
|
|
294
|
+
// ─── New chunk routes ────────────────────────────────────────────────────────
|
|
295
|
+
app.get("/api/chunks/all", (req, res) => {
|
|
296
|
+
const active = activeProject();
|
|
297
|
+
if (!active) {
|
|
298
|
+
res.status(503).json({ error: "No active project" });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const db = new EmbeddingsDb(active.embeddingsDb);
|
|
303
|
+
let chunks = db.getAllChunks();
|
|
304
|
+
db.close();
|
|
305
|
+
const sf = req.query["sourceFile"];
|
|
306
|
+
if (typeof sf === "string") {
|
|
307
|
+
chunks = chunks.filter((c) => c.sourceFile === sf);
|
|
308
|
+
}
|
|
309
|
+
res.json({ chunks });
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
res.status(500).json({ error: err.message });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
app.delete("/api/chunks/file", (req, res) => {
|
|
316
|
+
const body = req.body;
|
|
317
|
+
if (typeof body.sourceFile !== "string" || !body.sourceFile.trim()) {
|
|
318
|
+
res.status(400).json({ error: "sourceFile is required" });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const active = activeProject();
|
|
322
|
+
if (!active) {
|
|
323
|
+
res.status(503).json({ error: "No active project" });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const db = new EmbeddingsDb(active.embeddingsDb);
|
|
328
|
+
db.deleteBySourceFile(body.sourceFile.trim());
|
|
329
|
+
db.close();
|
|
330
|
+
res.json({ ok: true });
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
res.status(500).json({ error: err.message });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
app.delete("/api/chunks/all", (_req, res) => {
|
|
337
|
+
const active = activeProject();
|
|
338
|
+
if (!active) {
|
|
339
|
+
res.status(503).json({ error: "No active project" });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const db = new EmbeddingsDb(active.embeddingsDb);
|
|
344
|
+
db.clearAll();
|
|
345
|
+
db.close();
|
|
346
|
+
res.json({ ok: true });
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
res.status(500).json({ error: err.message });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
// ─── RAG search route ────────────────────────────────────────────────────────
|
|
353
|
+
app.post("/api/search", async (req, res) => {
|
|
354
|
+
const body = req.body;
|
|
355
|
+
if (typeof body.query !== "string" || !body.query.trim()) {
|
|
356
|
+
res.status(400).json({ error: "query is required" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const active = activeProject();
|
|
360
|
+
if (!active) {
|
|
361
|
+
res.status(503).json({ error: "No active project" });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const configPath = join(active.rootPath, "virage.config.json");
|
|
365
|
+
if (!existsSync(configPath)) {
|
|
366
|
+
res
|
|
367
|
+
.status(422)
|
|
368
|
+
.json({ error: `virage.config.json not found in ${active.rootPath}` });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const cfg = await getCachedConfig(configPath);
|
|
373
|
+
const topK = typeof body.topK === "number" ? body.topK : 5;
|
|
374
|
+
const embedding = await cfg.embedder.embed(body.query.trim());
|
|
375
|
+
const results = await cfg.vectorStore.search(embedding, topK);
|
|
376
|
+
res.json({ results });
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
res.status(500).json({ error: err.message });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// ─── Experiment routes ───────────────────────────────────────────────────────
|
|
383
|
+
const ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
384
|
+
app.get("/api/experiments", async (_req, res) => {
|
|
385
|
+
const active = activeProject();
|
|
386
|
+
if (!active) {
|
|
387
|
+
res.status(503).json({ error: "No active project" });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const store = new ExperimentStore(join(active.rootPath, ".rag-experiments"));
|
|
392
|
+
const runs = await store.list();
|
|
393
|
+
res.json({ runs });
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
res.status(500).json({ error: err.message });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
app.get("/api/experiments/:id", async (req, res) => {
|
|
400
|
+
if (!ID_RE.test(req.params["id"] ?? "")) {
|
|
401
|
+
res.status(400).json({ error: "Invalid experiment id" });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const active = activeProject();
|
|
405
|
+
if (!active) {
|
|
406
|
+
res.status(503).json({ error: "No active project" });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const store = new ExperimentStore(join(active.rootPath, ".rag-experiments"));
|
|
411
|
+
const run = await store.load(req.params["id"]);
|
|
412
|
+
res.json(run);
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
res.status(404).json({ error: err.message });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
app.delete("/api/experiments/:id", async (req, res) => {
|
|
419
|
+
if (!ID_RE.test(req.params["id"] ?? "")) {
|
|
420
|
+
res.status(400).json({ error: "Invalid experiment id" });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const active = activeProject();
|
|
424
|
+
if (!active) {
|
|
425
|
+
res.status(503).json({ error: "No active project" });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const filePath = join(active.rootPath, ".rag-experiments", `${req.params["id"]}.json`);
|
|
430
|
+
await unlink(filePath);
|
|
431
|
+
res.json({ ok: true });
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
res.status(500).json({ error: err.message });
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
app.post("/api/experiments/compare", async (req, res) => {
|
|
438
|
+
const body = req.body;
|
|
439
|
+
if (typeof body.baseline !== "string" ||
|
|
440
|
+
typeof body.candidate !== "string") {
|
|
441
|
+
res
|
|
442
|
+
.status(400)
|
|
443
|
+
.json({ error: "baseline and candidate ids are required" });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const active = activeProject();
|
|
447
|
+
if (!active) {
|
|
448
|
+
res.status(503).json({ error: "No active project" });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const store = new ExperimentStore(join(active.rootPath, ".rag-experiments"));
|
|
453
|
+
const [bRun, cRun] = await Promise.all([
|
|
454
|
+
store.load(body.baseline),
|
|
455
|
+
store.load(body.candidate),
|
|
456
|
+
]);
|
|
457
|
+
if (!bRun.perQueryRrScores || !cRun.perQueryRrScores) {
|
|
458
|
+
res
|
|
459
|
+
.status(422)
|
|
460
|
+
.json({ error: "Per-query scores unavailable for comparison" });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const result = bootstrapPairedTest(bRun.perQueryRrScores, cRun.perQueryRrScores);
|
|
464
|
+
res.json(result);
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
res.status(500).json({ error: err.message });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// ─── Static assets + SPA fallback ───────────────────────────────────────────
|
|
471
|
+
if (HAS_UI) {
|
|
472
|
+
app.use(express.static(UI_DIR));
|
|
473
|
+
app.get("*", (_req, res) => {
|
|
474
|
+
res.sendFile(join(UI_DIR, "index.html"));
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
// ─── Start server + attach WebSocket ────────────────────────────────────────
|
|
478
|
+
const server = app.listen(opts.port, () => {
|
|
479
|
+
console.log(`🚀 RAG Dashboard running at http://localhost:${opts.port}`);
|
|
480
|
+
console.log(" Press Ctrl+C to stop");
|
|
481
|
+
});
|
|
482
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
483
|
+
wss.on("connection", (ws) => {
|
|
484
|
+
ws.on("message", (raw) => {
|
|
485
|
+
let msg;
|
|
486
|
+
try {
|
|
487
|
+
msg = JSON.parse(raw.toString());
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
safeSend(ws, { type: "error", message: "Invalid JSON" });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (wsOperationRunning) {
|
|
494
|
+
safeSend(ws, { type: "busy" });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
wsOperationRunning = true;
|
|
498
|
+
handleWsOperation(ws, msg.op ?? "").finally(() => {
|
|
499
|
+
wsOperationRunning = false;
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
await new Promise((resolve) => {
|
|
504
|
+
process.on("SIGINT", () => {
|
|
505
|
+
wss.close();
|
|
506
|
+
server.close();
|
|
507
|
+
resolve();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
276
511
|
//# sourceMappingURL=dashboard.js.map
|