wazap-mcp 0.18.1 → 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.
@@ -211,13 +211,15 @@ class UrlBackend {
211
211
  */
212
212
  export class EmbedEngine {
213
213
  target;
214
- constructor(target) {
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
- async embed(texts) {
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: texts.length === 1 ? texts[0] : texts }),
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 !== texts.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) => {
@@ -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) {
@@ -12,10 +12,12 @@ const MODEL_ALIASES = ["embeddinggemma-300m", "e5-base-multilingual"];
12
12
  const DEFAULT_MAX_ROWS = 50_000;
13
13
  const MIN_MAX_ROWS = 100;
14
14
  /**
15
- * Cosine floor for embeddinggemma-300m, measured on real history: noise sits
16
- * at ~0.45-0.54, real matches start ~0.5. e5-base needs its own calibration.
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.
17
19
  */
18
- const DEFAULT_MIN_SIMILARITY = 0.5;
20
+ const DEFAULT_MIN_SIMILARITY = 0.35;
19
21
  function parseEnabled(raw) {
20
22
  const value = stripPasted(raw ?? "").toLowerCase();
21
23
  if (OFF.has(value))
@@ -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
- const STATE_VERSION = 1;
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;
package/dist/whatsapp.js CHANGED
@@ -624,7 +624,7 @@ 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
628
  const minSimilarity = this.recallEnv instanceof WazapError ? undefined : this.recallEnv.minSimilarity;
629
629
  const hits = store
630
630
  .query({ vector: vector, chatId: scope, sinceMs: opts.sinceMs, untilMs: opts.untilMs, from, minSimilarity, limit })
@@ -1237,7 +1237,7 @@ export class WhatsAppService {
1237
1237
  try {
1238
1238
  const spec = EMBED_MODELS[this.recallEnv.model];
1239
1239
  this.recallStore = await RecallStore.open(join(this.paths.root, "recall"), spec, this.recallEnv.maxRows);
1240
- this.recallQueue = new RecallQueue(this.recallStore, (texts) => this.recallEmbed(texts));
1240
+ this.recallQueue = new RecallQueue(this.recallStore, (texts) => this.recallEmbed(texts, "document"));
1241
1241
  if (this.recallStore.count > 0)
1242
1242
  log(`recall index: ${this.recallStore.count} messages`);
1243
1243
  }
@@ -1270,9 +1270,9 @@ export class WhatsAppService {
1270
1270
  }
1271
1271
  return this.recallEngineP;
1272
1272
  }
1273
- async recallEmbed(texts) {
1273
+ async recallEmbed(texts, kind) {
1274
1274
  const engine = await this.recallEngine();
1275
- return engine.embed(texts);
1275
+ return engine.embed(texts, kind);
1276
1276
  }
1277
1277
  /**
1278
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.1",
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",