@melaya/runner 1.0.35 → 1.0.37
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/connection.js +145 -2
- package/dist/modelLoader.js +26 -30
- package/dist/pythonEnv.js +27 -0
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -15,9 +15,14 @@ import chalk from "chalk";
|
|
|
15
15
|
import ora from "ora";
|
|
16
16
|
import { spawn } from "child_process";
|
|
17
17
|
import { writeFileSync, mkdirSync } from "fs";
|
|
18
|
-
import { join } from "path";
|
|
19
|
-
import {
|
|
18
|
+
import { dirname, join } from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import { tmpdir, homedir } from "os";
|
|
20
21
|
import { startLocalRelay, setActiveProject } from "./localRelay.js";
|
|
22
|
+
// ESM equivalent of __dirname — required for the rag:ingest handler to
|
|
23
|
+
// locate the bundled localRagIngest.py inside the npm package.
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = dirname(__filename);
|
|
21
26
|
import { ensureSharedModules, getSharedDir } from "./sharedVendor.js";
|
|
22
27
|
import { startLumaBrowserBridge } from "./lumaBrowserBridge.js";
|
|
23
28
|
const HEARTBEAT_INTERVAL = 30_000;
|
|
@@ -374,6 +379,144 @@ export async function connect(opts) {
|
|
|
374
379
|
socket.emit("runner:runComplete", { runId: payload.runId, status: "failed" });
|
|
375
380
|
}
|
|
376
381
|
});
|
|
382
|
+
// ── RAG Mode B: local-folder ingest (privacy-preserving) ─────────────
|
|
383
|
+
// The user picks "Local folder" in the Agent Builder DocsTab and types
|
|
384
|
+
// a path on their machine (e.g. C:\Users\me\Documents\refs). The server
|
|
385
|
+
// routes the ingest to this handler. The runner reads the files locally,
|
|
386
|
+
// embeds via local Ollama, and persists the Qdrant store at
|
|
387
|
+
// ~/.melaya-runner/rag/<pipelineName>/store/. The text NEVER leaves the
|
|
388
|
+
// user's box — that's the entire point of Mode B.
|
|
389
|
+
//
|
|
390
|
+
// Hard refusal: if embedderProvider is "openai", we refuse with a clear
|
|
391
|
+
// error message. Cloud embedders would defeat the data-residency
|
|
392
|
+
// guarantee. Mode B = local-only embedder, period.
|
|
393
|
+
socket.on("rag:ingest", async (payload) => {
|
|
394
|
+
const sid = payload?.sessionId;
|
|
395
|
+
if (!sid)
|
|
396
|
+
return;
|
|
397
|
+
const emitProgress = (kind, fields = {}) => {
|
|
398
|
+
socket.emit("rag:ingest-progress", { session_id: sid, kind, ...fields });
|
|
399
|
+
};
|
|
400
|
+
try {
|
|
401
|
+
const prov = (payload.embedderProvider || "").toLowerCase();
|
|
402
|
+
if (prov === "openai") {
|
|
403
|
+
const reason = ("Mode B (local folder) refuses cloud embedders. " +
|
|
404
|
+
"Pick ollama or lmstudio to keep your documents private.");
|
|
405
|
+
console.log(chalk.red(` ✗ rag:ingest refused: ${reason}`));
|
|
406
|
+
socket.emit("rag:ingest-result", {
|
|
407
|
+
session_id: sid, ok: false, error: reason,
|
|
408
|
+
});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
console.log(chalk.hex("#10B981")(`\n 🔒 RAG local-folder ingest: ${payload.pipelineName} ` +
|
|
412
|
+
`← ${payload.localFolderPath} via ${prov}:${payload.embedderModel}\n`));
|
|
413
|
+
emitProgress("started");
|
|
414
|
+
// Reuse the same venv as runner:run — agentscope.rag + ollama +
|
|
415
|
+
// qdrant-client land here.
|
|
416
|
+
const { ensurePythonEnv } = await import("./pythonEnv.js");
|
|
417
|
+
const env = await ensurePythonEnv(opts.pythonPath, payload.sharedVersion, (m) => emitProgress("venv", { message: m }));
|
|
418
|
+
if (!env.ok) {
|
|
419
|
+
emitProgress("error", { error: `venv bootstrap failed: ${env.reason}` });
|
|
420
|
+
socket.emit("rag:ingest-result", {
|
|
421
|
+
session_id: sid, ok: false, error: `venv bootstrap failed: ${env.reason}`,
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Stage the ingest script into the venv work dir so the spawned
|
|
426
|
+
// python sees the shared.runtime + the agentscope vendored tree.
|
|
427
|
+
await ensureSharedModules(opts.serverUrl, payload.sharedVersion);
|
|
428
|
+
const sharedDir = getSharedDir();
|
|
429
|
+
const ingestScript = join(sharedDir, "..", "localRagIngest.py");
|
|
430
|
+
// The script is published alongside the runner's shared bundle in
|
|
431
|
+
// the npm package itself — we copy it to a tmp dir per-invocation
|
|
432
|
+
// so re-runs are clean.
|
|
433
|
+
const workDir = join(tmpdir(), `melaya-rag-ingest-${payload.pipelineName}-${Date.now()}`);
|
|
434
|
+
mkdirSync(workDir, { recursive: true });
|
|
435
|
+
const { copyFileSync, existsSync } = await import("fs");
|
|
436
|
+
// Fallback path: when running from npm package layout
|
|
437
|
+
const candidates = [
|
|
438
|
+
ingestScript,
|
|
439
|
+
join(__dirname, "localRagIngest.py"),
|
|
440
|
+
join(__dirname, "..", "localRagIngest.py"),
|
|
441
|
+
];
|
|
442
|
+
let foundIngest = "";
|
|
443
|
+
for (const c of candidates) {
|
|
444
|
+
if (existsSync(c)) {
|
|
445
|
+
foundIngest = c;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (!foundIngest) {
|
|
450
|
+
const msg = "localRagIngest.py not found in runner package — reinstall @melaya/runner";
|
|
451
|
+
emitProgress("error", { error: msg });
|
|
452
|
+
socket.emit("rag:ingest-result", {
|
|
453
|
+
session_id: sid, ok: false, error: msg,
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const stagedScript = join(workDir, "localRagIngest.py");
|
|
458
|
+
copyFileSync(foundIngest, stagedScript);
|
|
459
|
+
// Resolve the on-disk path where the store will live. The Python
|
|
460
|
+
// script can compute this itself from pipelineName, but we surface
|
|
461
|
+
// it here for the FE event so the user can see where their data
|
|
462
|
+
// lives.
|
|
463
|
+
const storePath = join(homedir(), ".melaya-runner", "rag", payload.pipelineName, "store");
|
|
464
|
+
console.log(chalk.gray(` [rag] store → ${storePath}`));
|
|
465
|
+
const proc = spawn(env.pythonPath, [
|
|
466
|
+
"-u", stagedScript,
|
|
467
|
+
"--pipeline-name", payload.pipelineName,
|
|
468
|
+
"--folder", payload.localFolderPath,
|
|
469
|
+
"--embedder-provider", prov,
|
|
470
|
+
"--embedder-model", payload.embedderModel,
|
|
471
|
+
], {
|
|
472
|
+
env: {
|
|
473
|
+
...process.env,
|
|
474
|
+
PYTHONPATH: sharedDir, // so `from agentscope.rag import …` resolves
|
|
475
|
+
},
|
|
476
|
+
cwd: workDir,
|
|
477
|
+
});
|
|
478
|
+
let stdoutBuf = "";
|
|
479
|
+
let stderrBuf = "";
|
|
480
|
+
proc.stdout.on("data", (data) => {
|
|
481
|
+
stdoutBuf += data.toString("utf-8");
|
|
482
|
+
// Stream each "progress: ..." JSON line up to the FE
|
|
483
|
+
const lines = stdoutBuf.split("\n");
|
|
484
|
+
stdoutBuf = lines.pop() || "";
|
|
485
|
+
for (const line of lines) {
|
|
486
|
+
if (line.startsWith("progress: ")) {
|
|
487
|
+
try {
|
|
488
|
+
const ev = JSON.parse(line.slice("progress: ".length));
|
|
489
|
+
emitProgress("step", ev);
|
|
490
|
+
}
|
|
491
|
+
catch { /* ignore malformed */ }
|
|
492
|
+
}
|
|
493
|
+
else if (opts.verbose && line.trim()) {
|
|
494
|
+
console.log(chalk.gray(` [ingest] ${line}`));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
proc.stderr.on("data", (data) => {
|
|
499
|
+
stderrBuf += data.toString("utf-8");
|
|
500
|
+
});
|
|
501
|
+
proc.on("close", (code) => {
|
|
502
|
+
const ok = code === 0;
|
|
503
|
+
console.log(ok
|
|
504
|
+
? chalk.green(` ✓ RAG ingest complete (exit ${code})`)
|
|
505
|
+
: chalk.red(` ✗ RAG ingest failed (exit ${code})\n${stderrBuf.slice(-1000)}`));
|
|
506
|
+
socket.emit("rag:ingest-result", {
|
|
507
|
+
session_id: sid, ok, exit: code,
|
|
508
|
+
store_path: storePath,
|
|
509
|
+
stderr_tail: ok ? "" : stderrBuf.slice(-2000),
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
console.log(chalk.red(` ✗ rag:ingest crashed: ${e?.message || e}`));
|
|
515
|
+
socket.emit("rag:ingest-result", {
|
|
516
|
+
session_id: sid, ok: false, error: e?.message || String(e),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
});
|
|
377
520
|
// ── LinkedIn cookie capture (one-time) ────────────────────────────
|
|
378
521
|
// When the user clicks "Connect LinkedIn" in the platform UI, the
|
|
379
522
|
// server emits this event. We launch a controlled Chromium window on
|
package/dist/modelLoader.js
CHANGED
|
@@ -382,42 +382,38 @@ export async function preflightModel(configJson, onProgress = (m) => console.log
|
|
|
382
382
|
catch {
|
|
383
383
|
return { ok: true }; /* let main.py error naturally */
|
|
384
384
|
}
|
|
385
|
-
// Pipeline-level + per-step / per-agent
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
385
|
+
// Pipeline-level + per-step / per-agent (provider, model) PAIRS — must
|
|
386
|
+
// stay paired so a melaya_ai / openai / anthropic agent isn't checked
|
|
387
|
+
// against LM Studio just because a different agent in the same pipeline
|
|
388
|
+
// happens to use LM Studio. Mixed-provider pipelines were preflighting
|
|
389
|
+
// every cloud model against every local runner → spurious 'not found
|
|
390
|
+
// in LM Studio' failures.
|
|
391
|
+
const pairs = new Set(); // serialized "provider|model" for dedup
|
|
392
|
+
const addPair = (p, m) => {
|
|
393
|
+
if (p && m)
|
|
394
|
+
pairs.add(`${p.toLowerCase()}|${m}`);
|
|
395
|
+
};
|
|
396
|
+
addPair(String(cfg.model_provider ?? cfg.model?.provider ?? ""), String(cfg.model_name ?? cfg.model?.name ?? ""));
|
|
396
397
|
for (const step of (cfg.steps ?? [])) {
|
|
397
398
|
const a = step?.agent ?? {};
|
|
398
|
-
|
|
399
|
-
const n = String(a?.model?.name ?? "");
|
|
400
|
-
if (p)
|
|
401
|
-
providers.add(p);
|
|
402
|
-
if (n)
|
|
403
|
-
models.add(n);
|
|
399
|
+
addPair(String(a?.model?.provider ?? a?.model_provider ?? ""), String(a?.model?.name ?? a?.model_name ?? ""));
|
|
404
400
|
}
|
|
405
401
|
for (const a of (cfg.agents ?? [])) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (n)
|
|
411
|
-
models.add(n);
|
|
402
|
+
// Nested wins over flat — the ModelPicker writes `agent.model`
|
|
403
|
+
// but doesn't always purge the legacy `agent.model_provider` /
|
|
404
|
+
// `agent.model_name` fields, so they drift stale across edits.
|
|
405
|
+
addPair(String(a?.model?.provider ?? a?.model_provider ?? ""), String(a?.model?.name ?? a?.model_name ?? ""));
|
|
412
406
|
}
|
|
413
|
-
// Run preflight for
|
|
414
|
-
//
|
|
407
|
+
// Run preflight only for LOCAL providers — cloud (openai / anthropic /
|
|
408
|
+
// claude_code / codex / melaya_ai / zhipu) doesn't need the runner.
|
|
415
409
|
const tasks = [];
|
|
416
|
-
for (const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
410
|
+
for (const pair of pairs) {
|
|
411
|
+
const [provider, model] = pair.split("|");
|
|
412
|
+
if (provider === "lmstudio")
|
|
413
|
+
tasks.push(preflightLmStudio(model, onProgress));
|
|
414
|
+
else if (provider === "ollama")
|
|
415
|
+
tasks.push(preflightOllama(model, onProgress));
|
|
416
|
+
// every other provider is cloud-only — no preflight needed.
|
|
421
417
|
}
|
|
422
418
|
if (tasks.length === 0)
|
|
423
419
|
return { ok: true };
|
package/dist/pythonEnv.js
CHANGED
|
@@ -30,6 +30,17 @@ const AGENTSCOPE = join(CACHE_DIR, "agentscope");
|
|
|
30
30
|
// PLUS the runner-tool extras (aiohttp, requests, python-dotenv).
|
|
31
31
|
// If you bump the agentscope pyproject deps, bump this list too —
|
|
32
32
|
// or extract from pyproject at server-side and ship via the bundle.
|
|
33
|
+
// ── TODO(arch): move this list into the server's shared-bundle response ──
|
|
34
|
+
// Right now any new Python dep required by a shared tool means:
|
|
35
|
+
// 1. add the dep to this hardcoded list
|
|
36
|
+
// 2. bump the @melaya/runner npm version
|
|
37
|
+
// 3. publish + each user reinstalls
|
|
38
|
+
// The cleaner pattern is to ship a `requirements.txt` (or a JSON list) IN
|
|
39
|
+
// the shared-bundle endpoint, alongside the .py files. The runner then
|
|
40
|
+
// reads it at venv-bootstrap time. Result: adding a tool's Python dep
|
|
41
|
+
// only needs a server deploy — runners pick it up next reconnect, same
|
|
42
|
+
// way they pick up shared/*.py changes today.
|
|
43
|
+
// Until that lands, every change to this list = a runner npm version bump.
|
|
33
44
|
const PIP_DEPS = [
|
|
34
45
|
// ── agentscope core (mirrors pyproject.toml `dependencies`) ──
|
|
35
46
|
"aioitertools",
|
|
@@ -81,6 +92,22 @@ const PIP_DEPS = [
|
|
|
81
92
|
// `scrapling install` to download Chromium — best-effort run below
|
|
82
93
|
// (failure non-fatal: AsyncFetcher fast-path still works without it).
|
|
83
94
|
"scrapling[fetchers]",
|
|
95
|
+
// Telethon — Telegram MTProto user-mode client. Used by
|
|
96
|
+
// shared.tools.telegram_user_tools. The bot-API path in
|
|
97
|
+
// shared.tools.messaging.py uses plain HTTP and needs no extra dep.
|
|
98
|
+
"telethon",
|
|
99
|
+
// ── Vector RAG (retrieval mode) ────────────────────────────────────────
|
|
100
|
+
// ollama — Python client used by agentscope.embedding.OllamaTextEmbedding.
|
|
101
|
+
// Talks to localhost:11434 (the user's local Ollama daemon) to embed
|
|
102
|
+
// chunks without exfiltrating the underlying text to a cloud API. This
|
|
103
|
+
// is the only embedder allowed in privacy-preserving Mode B.
|
|
104
|
+
//
|
|
105
|
+
// (qdrant-client is ALREADY in this list above — it provides the
|
|
106
|
+
// embedded Qdrant store via agentscope.rag._store.QdrantStore at the
|
|
107
|
+
// ~/.melaya-runner/rag/<pipeline_name>/store/ path. Path-based Qdrant
|
|
108
|
+
// ships Windows wheels, MilvusLite does not — using Qdrant for OS
|
|
109
|
+
// parity across runners.)
|
|
110
|
+
"ollama",
|
|
84
111
|
];
|
|
85
112
|
export function venvPython() {
|
|
86
113
|
return platform() === "win32"
|