wazap-mcp 0.18.0 → 0.18.2
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/messages.js +3 -3
- package/dist/recall/engine.js +14 -6
- package/dist/recall/models.js +4 -0
- package/dist/recall/settings.js +17 -0
- package/dist/recall/store.js +4 -2
- package/dist/tools.js +8 -0
- package/dist/whatsapp.js +6 -5
- package/package.json +1 -1
package/dist/messages.js
CHANGED
|
@@ -399,9 +399,9 @@ export function searchableText(raw, transcript) {
|
|
|
399
399
|
if (type === "reaction" || type === "deleted" || type === "system")
|
|
400
400
|
return null;
|
|
401
401
|
const own = rule.text?.(node)?.trim() || rule.caption?.(node)?.trim() || rule.detail?.(node)?.trim() || "";
|
|
402
|
-
//
|
|
403
|
-
// noise that outranks real hits on short queries.
|
|
404
|
-
if (
|
|
402
|
+
// Under five letters or digits there is no meaning to embed — a "🥰🥰", a
|
|
403
|
+
// "Da" or a "..." only adds noise that outranks real hits on short queries.
|
|
404
|
+
if ((own.match(/[\p{L}\p{N}]/gu)?.length ?? 0) < 5 && spoken === undefined)
|
|
405
405
|
return null;
|
|
406
406
|
return viewText(raw, transcript);
|
|
407
407
|
}
|
package/dist/recall/engine.js
CHANGED
|
@@ -211,13 +211,15 @@ class UrlBackend {
|
|
|
211
211
|
*/
|
|
212
212
|
export class EmbedEngine {
|
|
213
213
|
target;
|
|
214
|
-
|
|
214
|
+
spec;
|
|
215
|
+
constructor(target, spec) {
|
|
215
216
|
this.target = target;
|
|
217
|
+
this.spec = spec;
|
|
216
218
|
}
|
|
217
219
|
static async start(settings, spec, onLog = () => { }) {
|
|
218
220
|
if (settings.embedUrl !== null) {
|
|
219
221
|
const base = settings.embedUrl.replace(/\/+$/, "");
|
|
220
|
-
return new EmbedEngine(new UrlBackend(base));
|
|
222
|
+
return new EmbedEngine(new UrlBackend(base), spec);
|
|
221
223
|
}
|
|
222
224
|
const bin = findLlama(settings);
|
|
223
225
|
if (bin === null) {
|
|
@@ -225,18 +227,24 @@ export class EmbedEngine {
|
|
|
225
227
|
}
|
|
226
228
|
const sidecar = new LlamaSidecar(bin, embedModelPath(settings.modelsDir, spec), onLog);
|
|
227
229
|
await sidecar.start();
|
|
228
|
-
return new EmbedEngine(sidecar);
|
|
230
|
+
return new EmbedEngine(sidecar, spec);
|
|
229
231
|
}
|
|
230
|
-
|
|
232
|
+
/**
|
|
233
|
+
* `kind` picks the model's task prefix: the index holds "document" texts,
|
|
234
|
+
* searches embed "query". The prefix is the model's side of a retrieval
|
|
235
|
+
* pair, not part of what the index stores.
|
|
236
|
+
*/
|
|
237
|
+
async embed(texts, kind) {
|
|
231
238
|
if (texts.length === 0)
|
|
232
239
|
return [];
|
|
233
240
|
await this.target.waitReady();
|
|
241
|
+
const input = texts.map((text) => `${this.spec.prompts[kind]}${text}`);
|
|
234
242
|
let response;
|
|
235
243
|
try {
|
|
236
244
|
response = await fetch(`${this.target.base}${EMBED_PATH}`, {
|
|
237
245
|
method: "POST",
|
|
238
246
|
headers: { "content-type": "application/json" },
|
|
239
|
-
body: JSON.stringify({ content:
|
|
247
|
+
body: JSON.stringify({ content: input.length === 1 ? input[0] : input }),
|
|
240
248
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
241
249
|
});
|
|
242
250
|
}
|
|
@@ -251,7 +259,7 @@ export class EmbedEngine {
|
|
|
251
259
|
if (!Array.isArray(reply)) {
|
|
252
260
|
throw new WazapError("RECALL_FAILED", `embedding server refused: ${reply.error?.message ?? "bad reply"}`);
|
|
253
261
|
}
|
|
254
|
-
if (reply.length !==
|
|
262
|
+
if (reply.length !== input.length) {
|
|
255
263
|
throw new WazapError("RECALL_FAILED", `embedding server returned ${reply.length} vectors for ${texts.length} texts`);
|
|
256
264
|
}
|
|
257
265
|
return reply.map((item, i) => {
|
package/dist/recall/models.js
CHANGED
|
@@ -22,6 +22,8 @@ export const EMBED_MODELS = {
|
|
|
22
22
|
sha256: "b5ce9d77a3fc4b3b39ccb5643c36777911cc4eb46a66962eadfa3f5f60490d63",
|
|
23
23
|
dims: 768,
|
|
24
24
|
url: "https://huggingface.co/ggml-org/embeddinggemma-300M-GGUF/resolve/main/embeddinggemma-300M-Q8_0.gguf",
|
|
25
|
+
// EmbeddingGemma's own retrieval task, from its model card.
|
|
26
|
+
prompts: { query: "task: search result | query: ", document: "title: none | text: " },
|
|
25
27
|
},
|
|
26
28
|
"e5-base-multilingual": {
|
|
27
29
|
alias: "e5-base-multilingual",
|
|
@@ -30,6 +32,8 @@ export const EMBED_MODELS = {
|
|
|
30
32
|
sha256: "548c31b068947aa26b86c8bbfc1f2fabe5233f6d0e1241319832b20a01e5968a",
|
|
31
33
|
dims: 768,
|
|
32
34
|
url: "https://huggingface.co/dinab/multilingual-e5-base-Q8_0-GGUF/resolve/main/multilingual-e5-base-q8_0.gguf",
|
|
35
|
+
// e5's documented asymmetric prefixes.
|
|
36
|
+
prompts: { query: "query: ", document: "passage: " },
|
|
33
37
|
},
|
|
34
38
|
};
|
|
35
39
|
export function embedModelSpec(name) {
|
package/dist/recall/settings.js
CHANGED
|
@@ -11,6 +11,13 @@ const ON = new Set(["local", "on", "1", "yes", "true"]);
|
|
|
11
11
|
const MODEL_ALIASES = ["embeddinggemma-300m", "e5-base-multilingual"];
|
|
12
12
|
const DEFAULT_MAX_ROWS = 50_000;
|
|
13
13
|
const MIN_MAX_ROWS = 100;
|
|
14
|
+
/**
|
|
15
|
+
* Cosine floor for embeddinggemma-300m under its task prompts, measured on a
|
|
16
|
+
* real index: noise tops out ~0.31, real paraphrases start ~0.35. The prompts
|
|
17
|
+
* widened the noise/signal gap enough for the floor to mean something.
|
|
18
|
+
* e5-base needs its own calibration.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_MIN_SIMILARITY = 0.35;
|
|
14
21
|
function parseEnabled(raw) {
|
|
15
22
|
const value = stripPasted(raw ?? "").toLowerCase();
|
|
16
23
|
if (OFF.has(value))
|
|
@@ -36,6 +43,15 @@ function parseMaxRows(raw) {
|
|
|
36
43
|
return n;
|
|
37
44
|
throw new WazapError("INVALID_ID", `WAZAP_RECALL_MAX must be a number >= ${MIN_MAX_ROWS}, got "${value}".`, "Fix WAZAP_RECALL_MAX or remove it");
|
|
38
45
|
}
|
|
46
|
+
function parseMinSimilarity(raw) {
|
|
47
|
+
const value = stripPasted(raw ?? "");
|
|
48
|
+
if (value === "")
|
|
49
|
+
return DEFAULT_MIN_SIMILARITY;
|
|
50
|
+
const n = Number(value);
|
|
51
|
+
if (Number.isFinite(n) && n >= 0 && n <= 1)
|
|
52
|
+
return n;
|
|
53
|
+
throw new WazapError("INVALID_ID", `WAZAP_RECALL_MIN_SIMILARITY must be a number between 0 and 1, got "${value}".`, "Fix WAZAP_RECALL_MIN_SIMILARITY or remove it");
|
|
54
|
+
}
|
|
39
55
|
function parseUrl(raw) {
|
|
40
56
|
const value = stripPasted(raw ?? "");
|
|
41
57
|
if (value === "")
|
|
@@ -59,5 +75,6 @@ export function readRecallSettings(env, dataDir) {
|
|
|
59
75
|
embedUrl: parseUrl(env.WAZAP_EMBED_URL),
|
|
60
76
|
modelsDir: join(dataDir, "models"),
|
|
61
77
|
maxRows: parseMaxRows(env.WAZAP_RECALL_MAX),
|
|
78
|
+
minSimilarity: parseMinSimilarity(env.WAZAP_RECALL_MIN_SIMILARITY),
|
|
62
79
|
};
|
|
63
80
|
}
|
package/dist/recall/store.js
CHANGED
|
@@ -21,7 +21,8 @@ import { WazapError } from "../errors.js";
|
|
|
21
21
|
export const TEXT_CAP = 2048;
|
|
22
22
|
/** Rewrite meta+vectors when more than this share of rows is dead. */
|
|
23
23
|
const COMPACT_DEAD_RATIO = 0.3;
|
|
24
|
-
|
|
24
|
+
/** v2: model task prompts — a raw-embedded index belongs to a different world. */
|
|
25
|
+
const STATE_VERSION = 2;
|
|
25
26
|
const QUANT = "int8";
|
|
26
27
|
/** The index holds message text; it gets history's permissions, not the defaults. */
|
|
27
28
|
const DIR_MODE = 0o700;
|
|
@@ -334,6 +335,7 @@ export class RecallStore {
|
|
|
334
335
|
*/
|
|
335
336
|
query(q, nowMs = Date.now()) {
|
|
336
337
|
const unit = normalize(q.vector);
|
|
338
|
+
const floor = q.minSimilarity ?? 0;
|
|
337
339
|
const hits = [];
|
|
338
340
|
for (const record of this.live.values()) {
|
|
339
341
|
if (q.chatId !== undefined && record.jid !== q.chatId)
|
|
@@ -349,7 +351,7 @@ export class RecallStore {
|
|
|
349
351
|
for (let i = 0; i < this.spec.dims; i++)
|
|
350
352
|
dot += unit[i] * this.vectors[offset + i];
|
|
351
353
|
const similarity = dot / 127;
|
|
352
|
-
if (similarity <= 0)
|
|
354
|
+
if (similarity <= 0 || similarity < floor)
|
|
353
355
|
continue;
|
|
354
356
|
hits.push({ record, similarity, score: similarity * recencyDecay(nowMs - record.ts) });
|
|
355
357
|
}
|
package/dist/tools.js
CHANGED
|
@@ -503,6 +503,8 @@ Each result carries its date and a score: semantic similarity scaled by
|
|
|
503
503
|
recency, so fresh matches rank first. chat_id, since, until and from narrow
|
|
504
504
|
the search exactly like search_messages. A hit marked "index only" lives in
|
|
505
505
|
the index alone: quote it, but get_message and download_media cannot see it.
|
|
506
|
+
Results under the similarity floor are dropped rather than listed; when only
|
|
507
|
+
weak matches survive, the output says so — do not present them as found facts.
|
|
506
508
|
|
|
507
509
|
RECALL_UNAVAILABLE means recall is off or the embedding setup is missing; the
|
|
508
510
|
fix names the command the user has to run. Do not retry it.`,
|
|
@@ -1121,7 +1123,13 @@ function renderRecall(title, answer) {
|
|
|
1121
1123
|
if (hits.length === 0) {
|
|
1122
1124
|
return `${title}: no messages found.${catchingUp ? ` ${catchingUp}` : ""}`;
|
|
1123
1125
|
}
|
|
1126
|
+
// Under ~0.55 cosine, embeddinggemma matches are usually coincidental — the
|
|
1127
|
+
// agent must not present them as found facts.
|
|
1128
|
+
const weak = Math.max(...hits.map((h) => h.similarity)) < 0.55;
|
|
1124
1129
|
const lines = [`# ${title} (${hits.length})`, ""];
|
|
1130
|
+
if (weak) {
|
|
1131
|
+
lines.push(`Weak matches only (best similarity ${Math.max(...hits.map((h) => h.similarity)).toFixed(2)}): the query may have no real answer — treat these as guesses.`, "");
|
|
1132
|
+
}
|
|
1125
1133
|
if (catchingUp)
|
|
1126
1134
|
lines.push(catchingUp, "");
|
|
1127
1135
|
const introduced = new Set();
|
package/dist/whatsapp.js
CHANGED
|
@@ -624,9 +624,10 @@ export class WhatsAppService {
|
|
|
624
624
|
const store = this.readyRecall();
|
|
625
625
|
const scope = chatId === undefined ? undefined : this.resolveId(chatId);
|
|
626
626
|
const from = opts.from === undefined ? undefined : opts.from === "me" ? this.ownJid() : this.resolveId(opts.from);
|
|
627
|
-
const [vector] = await this.recallEmbed([query]);
|
|
627
|
+
const [vector] = await this.recallEmbed([query], "query");
|
|
628
|
+
const minSimilarity = this.recallEnv instanceof WazapError ? undefined : this.recallEnv.minSimilarity;
|
|
628
629
|
const hits = store
|
|
629
|
-
.query({ vector: vector, chatId: scope, sinceMs: opts.sinceMs, untilMs: opts.untilMs, from, limit })
|
|
630
|
+
.query({ vector: vector, chatId: scope, sinceMs: opts.sinceMs, untilMs: opts.untilMs, from, minSimilarity, limit })
|
|
630
631
|
.map((hit) => {
|
|
631
632
|
const live = this.store.messages.has(hit.record.sid);
|
|
632
633
|
return {
|
|
@@ -1236,7 +1237,7 @@ export class WhatsAppService {
|
|
|
1236
1237
|
try {
|
|
1237
1238
|
const spec = EMBED_MODELS[this.recallEnv.model];
|
|
1238
1239
|
this.recallStore = await RecallStore.open(join(this.paths.root, "recall"), spec, this.recallEnv.maxRows);
|
|
1239
|
-
this.recallQueue = new RecallQueue(this.recallStore, (texts) => this.recallEmbed(texts));
|
|
1240
|
+
this.recallQueue = new RecallQueue(this.recallStore, (texts) => this.recallEmbed(texts, "document"));
|
|
1240
1241
|
if (this.recallStore.count > 0)
|
|
1241
1242
|
log(`recall index: ${this.recallStore.count} messages`);
|
|
1242
1243
|
}
|
|
@@ -1269,9 +1270,9 @@ export class WhatsAppService {
|
|
|
1269
1270
|
}
|
|
1270
1271
|
return this.recallEngineP;
|
|
1271
1272
|
}
|
|
1272
|
-
async recallEmbed(texts) {
|
|
1273
|
+
async recallEmbed(texts, kind) {
|
|
1273
1274
|
const engine = await this.recallEngine();
|
|
1274
|
-
return engine.embed(texts);
|
|
1275
|
+
return engine.embed(texts, kind);
|
|
1275
1276
|
}
|
|
1276
1277
|
/**
|
|
1277
1278
|
* The ops one raw message turns into: the tombstone a revoke carries for the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wazap-mcp",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"mcpName": "io.github.razvangirgiz/wazap",
|
|
5
5
|
"description": "WhatsApp for your AI agent. MCP server over Baileys: pairing-code login, several accounts, 33 tools, stdio or HTTP.",
|
|
6
6
|
"license": "MIT",
|