@melaya/runner 1.0.70 → 1.0.72
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 +107 -0
- package/dist/localRagRetrieve.py +162 -0
- package/dist/localRelay.js +42 -0
- package/localRagRetrieve.py +162 -0
- package/package.json +3 -2
package/dist/connection.js
CHANGED
|
@@ -1083,6 +1083,113 @@ export async function connect(opts) {
|
|
|
1083
1083
|
// connector. After that, the LinkedIn pipelines (auto-like / auto-reply
|
|
1084
1084
|
// / auto-connect) just read LINKEDIN_LI_AT / LINKEDIN_JSESSIONID from
|
|
1085
1085
|
// env at run time and call Voyager directly with aiohttp.
|
|
1086
|
+
socket.on("rag:retrieve", async (payload, ack) => {
|
|
1087
|
+
const sid = payload?.sessionId;
|
|
1088
|
+
if (!sid)
|
|
1089
|
+
return;
|
|
1090
|
+
ack?.({ ok: true });
|
|
1091
|
+
const embedOnly = payload.mode === "cloud_files";
|
|
1092
|
+
try {
|
|
1093
|
+
const prov = (payload.embedderProvider || "").toLowerCase();
|
|
1094
|
+
if (!["ollama", "lmstudio"].includes(prov)) {
|
|
1095
|
+
socket.emit("rag:retrieve-result", {
|
|
1096
|
+
session_id: sid,
|
|
1097
|
+
ok: false,
|
|
1098
|
+
error: "runner retrieval only supports ollama or lmstudio embedders",
|
|
1099
|
+
});
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const { ensurePythonEnv } = await import("./pythonEnv.js");
|
|
1103
|
+
const env = await ensurePythonEnv(opts.pythonPath, payload.sharedVersion);
|
|
1104
|
+
if (!env.ok) {
|
|
1105
|
+
socket.emit("rag:retrieve-result", {
|
|
1106
|
+
session_id: sid,
|
|
1107
|
+
ok: false,
|
|
1108
|
+
error: `venv bootstrap failed: ${env.reason}`,
|
|
1109
|
+
});
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
await ensureSharedModules(opts.serverUrl, payload.sharedVersion);
|
|
1113
|
+
const sharedDir = getSharedDir();
|
|
1114
|
+
const workDir = join(tmpdir(), `melaya-rag-retrieve-${payload.pipelineName}-${Date.now()}`);
|
|
1115
|
+
mkdirSync(workDir, { recursive: true });
|
|
1116
|
+
const { copyFileSync, existsSync } = await import("fs");
|
|
1117
|
+
const candidates = [
|
|
1118
|
+
join(sharedDir, "..", "localRagRetrieve.py"),
|
|
1119
|
+
join(__dirname, "localRagRetrieve.py"),
|
|
1120
|
+
join(__dirname, "..", "localRagRetrieve.py"),
|
|
1121
|
+
];
|
|
1122
|
+
let found = "";
|
|
1123
|
+
for (const c of candidates) {
|
|
1124
|
+
if (existsSync(c)) {
|
|
1125
|
+
found = c;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (!found) {
|
|
1130
|
+
socket.emit("rag:retrieve-result", {
|
|
1131
|
+
session_id: sid,
|
|
1132
|
+
ok: false,
|
|
1133
|
+
error: "localRagRetrieve.py not found in runner package - reinstall @melaya/runner",
|
|
1134
|
+
});
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const stagedScript = join(workDir, "localRagRetrieve.py");
|
|
1138
|
+
copyFileSync(found, stagedScript);
|
|
1139
|
+
const ragCertBundle = (await import("./pythonEnv.js")).getCertBundlePath();
|
|
1140
|
+
const ragSslEnv = ragCertBundle
|
|
1141
|
+
? { SSL_CERT_FILE: ragCertBundle, REQUESTS_CA_BUNDLE: ragCertBundle }
|
|
1142
|
+
: {};
|
|
1143
|
+
const proc = spawn(env.pythonPath, [
|
|
1144
|
+
"-u", stagedScript,
|
|
1145
|
+
"--pipeline-name", payload.pipelineName,
|
|
1146
|
+
"--embedder-provider", prov,
|
|
1147
|
+
"--embedder-model", payload.embedderModel,
|
|
1148
|
+
"--query", String(payload.query || ""),
|
|
1149
|
+
"--limit", String(Math.max(1, Math.min(20, Number(payload.limit ?? 5)))),
|
|
1150
|
+
// Mode C: embed only, print the vector; the server does the search.
|
|
1151
|
+
...(embedOnly ? ["--embed-only"] : []),
|
|
1152
|
+
], {
|
|
1153
|
+
env: {
|
|
1154
|
+
...process.env,
|
|
1155
|
+
PYTHONPATH: sharedDir,
|
|
1156
|
+
...ragSslEnv,
|
|
1157
|
+
},
|
|
1158
|
+
cwd: workDir,
|
|
1159
|
+
});
|
|
1160
|
+
let stdout = "";
|
|
1161
|
+
let stderr = "";
|
|
1162
|
+
proc.stdout.on("data", (data) => { stdout += data.toString("utf-8"); });
|
|
1163
|
+
proc.stderr.on("data", (data) => { stderr += data.toString("utf-8"); });
|
|
1164
|
+
proc.on("close", (code) => {
|
|
1165
|
+
const jsonLine = stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean).reverse()[0] || "";
|
|
1166
|
+
let parsed = null;
|
|
1167
|
+
try {
|
|
1168
|
+
parsed = JSON.parse(jsonLine);
|
|
1169
|
+
}
|
|
1170
|
+
catch { /* handled below */ }
|
|
1171
|
+
const ok = code === 0 && parsed?.ok === true;
|
|
1172
|
+
socket.emit("rag:retrieve-result", {
|
|
1173
|
+
session_id: sid,
|
|
1174
|
+
ok,
|
|
1175
|
+
// Mode C (embed-only): carry the query vector; the server searches
|
|
1176
|
+
// the prod store. Mode B: carry the local search results directly.
|
|
1177
|
+
vector: embedOnly && Array.isArray(parsed?.vector) ? parsed.vector : undefined,
|
|
1178
|
+
results: Array.isArray(parsed?.results) ? parsed.results : [],
|
|
1179
|
+
store_path: parsed?.store_path,
|
|
1180
|
+
error: ok ? undefined : (parsed?.error || `runner retrieve exit ${code ?? "?"}`),
|
|
1181
|
+
stderr_tail: stderr.slice(-2000),
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
catch (e) {
|
|
1186
|
+
socket.emit("rag:retrieve-result", {
|
|
1187
|
+
session_id: sid,
|
|
1188
|
+
ok: false,
|
|
1189
|
+
error: e?.message || String(e),
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1086
1193
|
socket.on("linkedin:start-login", async (req) => {
|
|
1087
1194
|
const sid = req?.session_id;
|
|
1088
1195
|
if (!sid)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Runner-side RAG retrieval test (local embedder paths).
|
|
2
|
+
|
|
3
|
+
Two modes, selected by --embed-only:
|
|
4
|
+
* Mode B (local folder, default): the vector store lives on THIS machine
|
|
5
|
+
(~/.melaya-runner/rag/<pipeline>/store), so we embed the query AND search
|
|
6
|
+
locally, returning the matched chunks.
|
|
7
|
+
* Mode C (cloud files, --embed-only): the chunks were upserted to the
|
|
8
|
+
PROD-side store during ingest, so they are NOT on this box. We only embed
|
|
9
|
+
the query with the user's local embedder and return the raw vector; the
|
|
10
|
+
server searches the prod store by that vector (see builder
|
|
11
|
+
/docs/retrieval/search_by_vector + runnerNamespace forwarding).
|
|
12
|
+
|
|
13
|
+
Either way the embedder is local (ollama / lmstudio), which is why this can't
|
|
14
|
+
run server-side: prod has no LM Studio / Ollama to embed the query.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import asyncio
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_store_path(pipeline_name: str) -> Path:
|
|
26
|
+
return Path.home() / ".melaya-runner" / "rag" / pipeline_name / "store"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_embedder(provider: str, model: str):
|
|
30
|
+
if provider == "ollama":
|
|
31
|
+
from agentscope.embedding import OllamaTextEmbedding
|
|
32
|
+
dims = {
|
|
33
|
+
"nomic-embed-text": 768,
|
|
34
|
+
"nomic-embed-text:v1.5": 768,
|
|
35
|
+
"mxbai-embed-large": 1024,
|
|
36
|
+
"all-minilm": 384,
|
|
37
|
+
"snowflake-arctic-embed": 1024,
|
|
38
|
+
}
|
|
39
|
+
dim = dims.get(model.lower())
|
|
40
|
+
if dim is None:
|
|
41
|
+
raise ValueError(f"unknown ollama embedding model '{model}'")
|
|
42
|
+
return OllamaTextEmbedding(model_name=model, dimensions=dim, host=None)
|
|
43
|
+
if provider == "lmstudio":
|
|
44
|
+
from agentscope.embedding import OpenAITextEmbedding
|
|
45
|
+
embedder = OpenAITextEmbedding(
|
|
46
|
+
api_key="lmstudio",
|
|
47
|
+
model_name=model,
|
|
48
|
+
dimensions=768,
|
|
49
|
+
base_url="http://localhost:1234/v1",
|
|
50
|
+
)
|
|
51
|
+
original_create = embedder.client.embeddings.create
|
|
52
|
+
|
|
53
|
+
async def _lms_create(**kwargs):
|
|
54
|
+
kwargs.pop("dimensions", None)
|
|
55
|
+
return await original_create(**kwargs)
|
|
56
|
+
|
|
57
|
+
embedder.client.embeddings.create = _lms_create # type: ignore[assignment]
|
|
58
|
+
return embedder
|
|
59
|
+
raise ValueError(f"unsupported embedder provider '{provider}'")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _extract_text(doc) -> str:
|
|
63
|
+
content = getattr(doc.metadata, "content", None)
|
|
64
|
+
if isinstance(content, dict):
|
|
65
|
+
return str(content.get("text") or "")
|
|
66
|
+
return str(getattr(content, "text", "") or "")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _embed_only(args) -> dict:
|
|
70
|
+
"""Mode C: embed the query with the user's LOCAL embedder and return the
|
|
71
|
+
raw vector. The vectors live in the prod-side store (Mode C ingest upserted
|
|
72
|
+
them there), so the SERVER does the search — this box only embeds. No store
|
|
73
|
+
is opened here."""
|
|
74
|
+
from agentscope.message import TextBlock
|
|
75
|
+
embedder = _build_embedder(args.embedder_provider, args.embedder_model)
|
|
76
|
+
res = await embedder([TextBlock(type="text", text=str(args.query or "")[:2000])])
|
|
77
|
+
vector = list(res.embeddings[0])
|
|
78
|
+
return {"ok": True, "vector": vector}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _retrieve(args) -> dict:
|
|
82
|
+
if getattr(args, "embed_only", False):
|
|
83
|
+
return await _embed_only(args)
|
|
84
|
+
store_path = _resolve_store_path(args.pipeline_name)
|
|
85
|
+
if not store_path.exists():
|
|
86
|
+
return {
|
|
87
|
+
"ok": False,
|
|
88
|
+
"error": f"runner RAG store not found: {store_path}",
|
|
89
|
+
"results": [],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
import qdrant_client as _qc
|
|
93
|
+
if not getattr(_qc.AsyncQdrantClient.__init__, "_melaya_patched", False):
|
|
94
|
+
original_init = _qc.AsyncQdrantClient.__init__
|
|
95
|
+
|
|
96
|
+
def _patched_init(self, location=None, **kwargs):
|
|
97
|
+
if (
|
|
98
|
+
location
|
|
99
|
+
and location != ":memory:"
|
|
100
|
+
and "://" not in str(location)
|
|
101
|
+
and not kwargs.get("path")
|
|
102
|
+
):
|
|
103
|
+
kwargs["path"] = location
|
|
104
|
+
location = None
|
|
105
|
+
original_init(self, location=location, **kwargs)
|
|
106
|
+
|
|
107
|
+
_patched_init._melaya_patched = True # type: ignore[attr-defined]
|
|
108
|
+
_qc.AsyncQdrantClient.__init__ = _patched_init # type: ignore[assignment]
|
|
109
|
+
|
|
110
|
+
from agentscope.rag import QdrantStore, SimpleKnowledge
|
|
111
|
+
|
|
112
|
+
embedder = _build_embedder(args.embedder_provider, args.embedder_model)
|
|
113
|
+
store = QdrantStore(
|
|
114
|
+
location=str(store_path),
|
|
115
|
+
collection_name=args.pipeline_name,
|
|
116
|
+
dimensions=embedder.dimensions,
|
|
117
|
+
distance="Cosine",
|
|
118
|
+
)
|
|
119
|
+
kb = SimpleKnowledge(embedding_store=store, embedding_model=embedder)
|
|
120
|
+
docs = await kb.retrieve(
|
|
121
|
+
query=str(args.query or "")[:2000],
|
|
122
|
+
limit=max(1, min(20, int(args.limit))),
|
|
123
|
+
)
|
|
124
|
+
results = []
|
|
125
|
+
for doc in docs:
|
|
126
|
+
score = getattr(doc, "score", None)
|
|
127
|
+
results.append({
|
|
128
|
+
"source": getattr(doc.metadata, "doc_id", "?"),
|
|
129
|
+
"chunk_id": getattr(doc.metadata, "chunk_id", "?"),
|
|
130
|
+
"score": score,
|
|
131
|
+
"text": _extract_text(doc),
|
|
132
|
+
})
|
|
133
|
+
return {
|
|
134
|
+
"ok": True,
|
|
135
|
+
"store_path": str(store_path),
|
|
136
|
+
"results": results,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main() -> None:
|
|
141
|
+
parser = argparse.ArgumentParser()
|
|
142
|
+
parser.add_argument("--pipeline-name", required=True)
|
|
143
|
+
parser.add_argument("--embedder-provider", required=True, choices=["ollama", "lmstudio"])
|
|
144
|
+
parser.add_argument("--embedder-model", required=True)
|
|
145
|
+
parser.add_argument("--query", required=True)
|
|
146
|
+
parser.add_argument("--limit", type=int, default=5)
|
|
147
|
+
# Mode C: embed the query and print {ok, vector}; do not open a local store.
|
|
148
|
+
parser.add_argument("--embed-only", dest="embed_only", action="store_true")
|
|
149
|
+
args = parser.parse_args()
|
|
150
|
+
try:
|
|
151
|
+
print(json.dumps(asyncio.run(_retrieve(args)), ensure_ascii=False))
|
|
152
|
+
except Exception as exc: # noqa: BLE001
|
|
153
|
+
print(json.dumps({
|
|
154
|
+
"ok": False,
|
|
155
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
156
|
+
"results": [],
|
|
157
|
+
}), file=sys.stdout)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
package/dist/localRelay.js
CHANGED
|
@@ -105,6 +105,48 @@ export function startLocalRelay(socket, verbose, serverUrl) {
|
|
|
105
105
|
});
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
|
+
// ── Mode C RAG search proxy ─────────────────────────────────────
|
|
109
|
+
// A runner-dispatched pipeline's `rag_retrieve` (Mode C: cloud store +
|
|
110
|
+
// local embedder) embeds the query locally then POSTs the vector to
|
|
111
|
+
// `${MEL_BUILDER_URL}/pipelines/<name>/docs/retrieval/search_by_vector`.
|
|
112
|
+
// We forward it over the authenticated socket (the runner is the trust
|
|
113
|
+
// anchor) to `runner:ragSearch`, which calls the server's
|
|
114
|
+
// _forwardSearchByVector → builder search_by_vector on the prod store.
|
|
115
|
+
const sm = req.method === "POST" && req.url
|
|
116
|
+
? req.url.match(/^\/pipelines\/([^/]+)\/docs\/retrieval\/search_by_vector\/?$/)
|
|
117
|
+
: null;
|
|
118
|
+
if (sm) {
|
|
119
|
+
const pipelineName = decodeURIComponent(sm[1]);
|
|
120
|
+
let sbody = "";
|
|
121
|
+
req.on("data", (chunk) => { sbody += chunk.toString(); });
|
|
122
|
+
req.on("end", () => {
|
|
123
|
+
const replyOnce = (status, body) => {
|
|
124
|
+
if (res.headersSent)
|
|
125
|
+
return;
|
|
126
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
127
|
+
res.end(JSON.stringify(body));
|
|
128
|
+
};
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(sbody || "{}");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
replyOnce(400, { results: [], error: "invalid_json" });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Relay timeout < the pipeline's urlopen timeout (30s) so the
|
|
138
|
+
// pipeline always gets a JSON reply rather than its own abort.
|
|
139
|
+
const t = setTimeout(() => replyOnce(504, { results: [], error: "upstream_timeout" }), 25000);
|
|
140
|
+
socket.emit("runner:ragSearch", { name: pipelineName, vector: parsed.vector, limit: parsed.limit }, (resp) => {
|
|
141
|
+
clearTimeout(t);
|
|
142
|
+
if (resp && typeof resp === "object")
|
|
143
|
+
replyOnce(200, resp);
|
|
144
|
+
else
|
|
145
|
+
replyOnce(502, { results: [], error: "no_response" });
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
108
150
|
if (req.method !== "POST" || req.url !== expectedPath) {
|
|
109
151
|
res.writeHead(404);
|
|
110
152
|
res.end("Not found");
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Runner-side RAG retrieval test (local embedder paths).
|
|
2
|
+
|
|
3
|
+
Two modes, selected by --embed-only:
|
|
4
|
+
* Mode B (local folder, default): the vector store lives on THIS machine
|
|
5
|
+
(~/.melaya-runner/rag/<pipeline>/store), so we embed the query AND search
|
|
6
|
+
locally, returning the matched chunks.
|
|
7
|
+
* Mode C (cloud files, --embed-only): the chunks were upserted to the
|
|
8
|
+
PROD-side store during ingest, so they are NOT on this box. We only embed
|
|
9
|
+
the query with the user's local embedder and return the raw vector; the
|
|
10
|
+
server searches the prod store by that vector (see builder
|
|
11
|
+
/docs/retrieval/search_by_vector + runnerNamespace forwarding).
|
|
12
|
+
|
|
13
|
+
Either way the embedder is local (ollama / lmstudio), which is why this can't
|
|
14
|
+
run server-side: prod has no LM Studio / Ollama to embed the query.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import asyncio
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_store_path(pipeline_name: str) -> Path:
|
|
26
|
+
return Path.home() / ".melaya-runner" / "rag" / pipeline_name / "store"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_embedder(provider: str, model: str):
|
|
30
|
+
if provider == "ollama":
|
|
31
|
+
from agentscope.embedding import OllamaTextEmbedding
|
|
32
|
+
dims = {
|
|
33
|
+
"nomic-embed-text": 768,
|
|
34
|
+
"nomic-embed-text:v1.5": 768,
|
|
35
|
+
"mxbai-embed-large": 1024,
|
|
36
|
+
"all-minilm": 384,
|
|
37
|
+
"snowflake-arctic-embed": 1024,
|
|
38
|
+
}
|
|
39
|
+
dim = dims.get(model.lower())
|
|
40
|
+
if dim is None:
|
|
41
|
+
raise ValueError(f"unknown ollama embedding model '{model}'")
|
|
42
|
+
return OllamaTextEmbedding(model_name=model, dimensions=dim, host=None)
|
|
43
|
+
if provider == "lmstudio":
|
|
44
|
+
from agentscope.embedding import OpenAITextEmbedding
|
|
45
|
+
embedder = OpenAITextEmbedding(
|
|
46
|
+
api_key="lmstudio",
|
|
47
|
+
model_name=model,
|
|
48
|
+
dimensions=768,
|
|
49
|
+
base_url="http://localhost:1234/v1",
|
|
50
|
+
)
|
|
51
|
+
original_create = embedder.client.embeddings.create
|
|
52
|
+
|
|
53
|
+
async def _lms_create(**kwargs):
|
|
54
|
+
kwargs.pop("dimensions", None)
|
|
55
|
+
return await original_create(**kwargs)
|
|
56
|
+
|
|
57
|
+
embedder.client.embeddings.create = _lms_create # type: ignore[assignment]
|
|
58
|
+
return embedder
|
|
59
|
+
raise ValueError(f"unsupported embedder provider '{provider}'")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _extract_text(doc) -> str:
|
|
63
|
+
content = getattr(doc.metadata, "content", None)
|
|
64
|
+
if isinstance(content, dict):
|
|
65
|
+
return str(content.get("text") or "")
|
|
66
|
+
return str(getattr(content, "text", "") or "")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _embed_only(args) -> dict:
|
|
70
|
+
"""Mode C: embed the query with the user's LOCAL embedder and return the
|
|
71
|
+
raw vector. The vectors live in the prod-side store (Mode C ingest upserted
|
|
72
|
+
them there), so the SERVER does the search — this box only embeds. No store
|
|
73
|
+
is opened here."""
|
|
74
|
+
from agentscope.message import TextBlock
|
|
75
|
+
embedder = _build_embedder(args.embedder_provider, args.embedder_model)
|
|
76
|
+
res = await embedder([TextBlock(type="text", text=str(args.query or "")[:2000])])
|
|
77
|
+
vector = list(res.embeddings[0])
|
|
78
|
+
return {"ok": True, "vector": vector}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _retrieve(args) -> dict:
|
|
82
|
+
if getattr(args, "embed_only", False):
|
|
83
|
+
return await _embed_only(args)
|
|
84
|
+
store_path = _resolve_store_path(args.pipeline_name)
|
|
85
|
+
if not store_path.exists():
|
|
86
|
+
return {
|
|
87
|
+
"ok": False,
|
|
88
|
+
"error": f"runner RAG store not found: {store_path}",
|
|
89
|
+
"results": [],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
import qdrant_client as _qc
|
|
93
|
+
if not getattr(_qc.AsyncQdrantClient.__init__, "_melaya_patched", False):
|
|
94
|
+
original_init = _qc.AsyncQdrantClient.__init__
|
|
95
|
+
|
|
96
|
+
def _patched_init(self, location=None, **kwargs):
|
|
97
|
+
if (
|
|
98
|
+
location
|
|
99
|
+
and location != ":memory:"
|
|
100
|
+
and "://" not in str(location)
|
|
101
|
+
and not kwargs.get("path")
|
|
102
|
+
):
|
|
103
|
+
kwargs["path"] = location
|
|
104
|
+
location = None
|
|
105
|
+
original_init(self, location=location, **kwargs)
|
|
106
|
+
|
|
107
|
+
_patched_init._melaya_patched = True # type: ignore[attr-defined]
|
|
108
|
+
_qc.AsyncQdrantClient.__init__ = _patched_init # type: ignore[assignment]
|
|
109
|
+
|
|
110
|
+
from agentscope.rag import QdrantStore, SimpleKnowledge
|
|
111
|
+
|
|
112
|
+
embedder = _build_embedder(args.embedder_provider, args.embedder_model)
|
|
113
|
+
store = QdrantStore(
|
|
114
|
+
location=str(store_path),
|
|
115
|
+
collection_name=args.pipeline_name,
|
|
116
|
+
dimensions=embedder.dimensions,
|
|
117
|
+
distance="Cosine",
|
|
118
|
+
)
|
|
119
|
+
kb = SimpleKnowledge(embedding_store=store, embedding_model=embedder)
|
|
120
|
+
docs = await kb.retrieve(
|
|
121
|
+
query=str(args.query or "")[:2000],
|
|
122
|
+
limit=max(1, min(20, int(args.limit))),
|
|
123
|
+
)
|
|
124
|
+
results = []
|
|
125
|
+
for doc in docs:
|
|
126
|
+
score = getattr(doc, "score", None)
|
|
127
|
+
results.append({
|
|
128
|
+
"source": getattr(doc.metadata, "doc_id", "?"),
|
|
129
|
+
"chunk_id": getattr(doc.metadata, "chunk_id", "?"),
|
|
130
|
+
"score": score,
|
|
131
|
+
"text": _extract_text(doc),
|
|
132
|
+
})
|
|
133
|
+
return {
|
|
134
|
+
"ok": True,
|
|
135
|
+
"store_path": str(store_path),
|
|
136
|
+
"results": results,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main() -> None:
|
|
141
|
+
parser = argparse.ArgumentParser()
|
|
142
|
+
parser.add_argument("--pipeline-name", required=True)
|
|
143
|
+
parser.add_argument("--embedder-provider", required=True, choices=["ollama", "lmstudio"])
|
|
144
|
+
parser.add_argument("--embedder-model", required=True)
|
|
145
|
+
parser.add_argument("--query", required=True)
|
|
146
|
+
parser.add_argument("--limit", type=int, default=5)
|
|
147
|
+
# Mode C: embed the query and print {ok, vector}; do not open a local store.
|
|
148
|
+
parser.add_argument("--embed-only", dest="embed_only", action="store_true")
|
|
149
|
+
args = parser.parse_args()
|
|
150
|
+
try:
|
|
151
|
+
print(json.dumps(asyncio.run(_retrieve(args)), ensure_ascii=False))
|
|
152
|
+
except Exception as exc: # noqa: BLE001
|
|
153
|
+
print(json.dumps({
|
|
154
|
+
"ok": False,
|
|
155
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
156
|
+
"results": [],
|
|
157
|
+
}), file=sys.stdout)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melaya/runner",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.72",
|
|
4
4
|
"description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
"dist/**/*.d.ts",
|
|
15
15
|
"dist/**/*.py",
|
|
16
16
|
"localRagIngest.py",
|
|
17
|
+
"localRagRetrieve.py",
|
|
17
18
|
"nltk_data/**",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
|
-
"build": "tsc && node -e \"require('fs').copyFileSync('localRagIngest.py','dist/localRagIngest.py')\"",
|
|
22
|
+
"build": "tsc && node -e \"const fs=require('fs'); fs.copyFileSync('localRagIngest.py','dist/localRagIngest.py'); fs.copyFileSync('localRagRetrieve.py','dist/localRagRetrieve.py')\"",
|
|
22
23
|
"prepublishOnly": "npm run build"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|