@amemhq/core 1.0.0 → 1.1.0

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/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
34
34
  DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
35
35
  EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
36
+ EmbeddingModelMismatchError: () => EmbeddingModelMismatchError,
36
37
  addEpisodic: () => addEpisodic,
37
38
  addMemory: () => addMemory,
38
39
  canRead: () => canRead,
@@ -47,8 +48,11 @@ __export(index_exports, {
47
48
  encode: () => encode,
48
49
  ensureCollection: () => ensureCollection,
49
50
  generateReviewBatch: () => generateReviewBatch,
51
+ getEmbeddingDevice: () => getEmbeddingDevice,
50
52
  getEmbeddingDim: () => getEmbeddingDim,
53
+ getEmbeddingDtype: () => getEmbeddingDtype,
51
54
  getEmbeddingModel: () => getEmbeddingModel,
55
+ getEmbeddingPooling: () => getEmbeddingPooling,
52
56
  getNote: () => getNote,
53
57
  invalidateNote: () => invalidateNote,
54
58
  isModelLoaded: () => isModelLoaded,
@@ -82,47 +86,85 @@ function getDataDir() {
82
86
  // src/embedding.ts
83
87
  var pipeline = null;
84
88
  var extractor = null;
85
- var loadedModelName = null;
89
+ var loadedKey = null;
86
90
  var cachedDim = null;
87
91
  var DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
88
92
  function getEmbeddingModel() {
89
93
  return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
90
94
  }
95
+ var CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
96
+ "bge-m3",
97
+ "bge-base-zh-v1.5",
98
+ "bge-small-zh-v1.5",
99
+ "bge-base-en-v1.5",
100
+ "bge-small-en-v1.5",
101
+ "bge-large-en-v1.5",
102
+ "gte-multilingual-base",
103
+ "gte-modernbert-base",
104
+ "gte-large-en-v1.5",
105
+ "snowflake-arctic-embed-m",
106
+ "snowflake-arctic-embed-l"
107
+ ]);
108
+ function getEmbeddingPooling() {
109
+ const explicit = process.env.AMEM_EMBED_POOLING?.trim().toLowerCase();
110
+ if (explicit === "mean" || explicit === "cls") return explicit;
111
+ const basename2 = getEmbeddingModel().split("/").pop()?.toLowerCase() ?? "";
112
+ return CLS_POOLED_MODELS.has(basename2) ? "cls" : "mean";
113
+ }
114
+ function getEmbeddingDevice() {
115
+ return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
116
+ }
117
+ function getEmbeddingDtype() {
118
+ return process.env.AMEM_EMBED_DTYPE?.trim() || void 0;
119
+ }
120
+ function extractorKey() {
121
+ return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
122
+ }
91
123
  async function getExtractor() {
92
- const wanted = getEmbeddingModel();
93
- if (extractor && loadedModelName === wanted) return extractor;
124
+ const wanted = extractorKey();
125
+ if (extractor && loadedKey === wanted) return extractor;
94
126
  if (!pipeline) {
95
127
  const mod = await import("@huggingface/transformers");
96
128
  pipeline = mod.pipeline;
97
129
  }
98
- extractor = await pipeline("feature-extraction", wanted, {
99
- revision: "main"
130
+ const device = getEmbeddingDevice();
131
+ const dtype = getEmbeddingDtype();
132
+ extractor = await pipeline("feature-extraction", getEmbeddingModel(), {
133
+ revision: "main",
134
+ // Omitted entirely when unset, so an unconfigured install gets exactly the
135
+ // library defaults it got before these existed.
136
+ ...device ? { device } : {},
137
+ ...dtype ? { dtype } : {}
100
138
  });
101
- loadedModelName = wanted;
139
+ loadedKey = wanted;
102
140
  cachedDim = null;
103
141
  return extractor;
104
142
  }
105
143
  async function getEmbeddingDim() {
106
- if (cachedDim !== null && loadedModelName === getEmbeddingModel()) return cachedDim;
144
+ if (cachedDim !== null && loadedKey === extractorKey()) return cachedDim;
107
145
  const probe = await encode("dimension probe");
108
146
  cachedDim = probe.length;
109
147
  return cachedDim;
110
148
  }
111
- function meanPoolingNormalize(output, attentionMask) {
149
+ function poolNormalize(output, attentionMask, mode) {
112
150
  const seqLen = output.length;
113
151
  const dim = output[0].length;
114
152
  const pooled = new Array(dim).fill(0);
115
- let maskSum = 0;
116
- for (let i = 0; i < seqLen; i++) {
117
- const m = attentionMask[i];
118
- maskSum += m;
153
+ if (mode === "cls") {
154
+ for (let j = 0; j < dim; j++) pooled[j] = output[0][j];
155
+ } else {
156
+ let maskSum = 0;
157
+ for (let i = 0; i < seqLen; i++) {
158
+ const m = attentionMask[i];
159
+ maskSum += m;
160
+ for (let j = 0; j < dim; j++) {
161
+ pooled[j] += output[i][j] * m;
162
+ }
163
+ }
119
164
  for (let j = 0; j < dim; j++) {
120
- pooled[j] += output[i][j] * m;
165
+ pooled[j] /= Math.max(maskSum, 1e-9);
121
166
  }
122
167
  }
123
- for (let j = 0; j < dim; j++) {
124
- pooled[j] /= Math.max(maskSum, 1e-9);
125
- }
126
168
  let norm = 0;
127
169
  for (const v of pooled) norm += v * v;
128
170
  norm = Math.sqrt(norm);
@@ -130,7 +172,8 @@ function meanPoolingNormalize(output, attentionMask) {
130
172
  }
131
173
  async function encode(text) {
132
174
  const ext = await getExtractor();
133
- const result = await ext(text, { pooling: "mean", normalize: true });
175
+ const pooling = getEmbeddingPooling();
176
+ const result = await ext(text, { pooling, normalize: true });
134
177
  if (result && result.data) {
135
178
  return Array.from(result.data);
136
179
  }
@@ -146,7 +189,7 @@ async function encode(text) {
146
189
  }
147
190
  raw.push(row);
148
191
  }
149
- return meanPoolingNormalize(raw, new Array(seqLen).fill(1));
192
+ return poolNormalize(raw, new Array(seqLen).fill(1), pooling);
150
193
  }
151
194
  throw new Error("Unexpected embedding output shape");
152
195
  }
@@ -183,7 +226,7 @@ var EmbeddingDimensionMismatchError = class extends Error {
183
226
  constructor(collection, collectionDim, modelDim, model) {
184
227
  super(
185
228
  `Collection "${collection}" stores ${collectionDim}-dimension vectors, but the embedding model "${model}" produces ${modelDim}. Qdrant fixes a collection's vector size at creation and cannot change it, so writes and searches would both fail.
186
- Either set AMEM_EMBED_MODEL back to the model this collection was built with, or migrate: build a new collection with the new model, backfill it, then point AMEM_COLLECTION at it. See docs/reference/embedding-models.md.`
229
+ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or ${migrationHint(collection, model)}`
187
230
  );
188
231
  this.collection = collection;
189
232
  this.collectionDim = collectionDim;
@@ -196,6 +239,35 @@ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or
196
239
  modelDim;
197
240
  model;
198
241
  };
242
+ function migrationHint(collection, targetModel) {
243
+ return `migrate to a new collection:
244
+
245
+ AMEM_EMBED_MODEL=${targetModel} \\
246
+ npx --package=@amemhq/core amem-migrate --to ${collection}_v2
247
+
248
+ That is a dry run; add --apply to write. "${collection}" is only read, so nothing is lost either way. When it looks right, point whatever names this collection at the new one \u2014 AMEM_COLLECTION, or the plugin's "collection" setting if this agent has its own. See https://amem.owo.lc/reference/embedding-models.`;
249
+ }
250
+ var EmbeddingModelMismatchError = class extends Error {
251
+ constructor(collection, collectionModel, configuredModel) {
252
+ super(
253
+ `Collection "${collection}" was built with the embedding model "${collectionModel}", but this process is configured for "${configuredModel}". Both produce vectors of the same width, so nothing would fail \u2014 searches would just quietly compare vectors from two different models.
254
+ Either set AMEM_EMBED_MODEL back to "${collectionModel}", or ` + migrationHint(collection, configuredModel)
255
+ );
256
+ this.collection = collection;
257
+ this.collectionModel = collectionModel;
258
+ this.configuredModel = configuredModel;
259
+ this.name = "EmbeddingModelMismatchError";
260
+ }
261
+ collection;
262
+ collectionModel;
263
+ configuredModel;
264
+ };
265
+ async function recordCollectionModel(collection, model) {
266
+ try {
267
+ await qdrant("PATCH", `/collections/${collection}`, { metadata: { embedding_model: model } });
268
+ } catch {
269
+ }
270
+ }
199
271
  async function qdrant(method, path5, body) {
200
272
  const res = await fetch(`${QDRANT_URL}${path5}`, {
201
273
  method,
@@ -238,6 +310,14 @@ async function ensureCollection(collectionName) {
238
310
  throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
239
311
  }
240
312
  }
313
+ const recorded = existing.config?.metadata?.embedding_model;
314
+ const current = getEmbeddingModel();
315
+ if (typeof recorded === "string" && recorded !== current) {
316
+ throw new EmbeddingModelMismatchError(col, recorded, current);
317
+ }
318
+ if (recorded === void 0) {
319
+ await recordCollectionModel(col, current);
320
+ }
241
321
  markReady();
242
322
  return;
243
323
  }
@@ -249,6 +329,7 @@ async function ensureCollection(collectionName) {
249
329
  } catch (err) {
250
330
  if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
251
331
  }
332
+ await recordCollectionModel(col, getEmbeddingModel());
252
333
  await qdrant("PUT", `/collections/${col}/index`, {
253
334
  field_name: "agent_id",
254
335
  field_schema: "keyword"
@@ -294,6 +375,7 @@ async function collectionDimRaw(collection) {
294
375
  }
295
376
  async function createCollectionRaw(collection, size) {
296
377
  await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
378
+ await recordCollectionModel(collection, getEmbeddingModel());
297
379
  for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
298
380
  await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
299
381
  }
@@ -2333,6 +2415,7 @@ function isPlausibleUpdateTarget(newEmbedding, targetEmbedding, minSimilarity) {
2333
2415
  DEFAULT_CRUD_UPDATE_MIN_SIM,
2334
2416
  DEFAULT_EMBEDDING_MODEL,
2335
2417
  EmbeddingDimensionMismatchError,
2418
+ EmbeddingModelMismatchError,
2336
2419
  addEpisodic,
2337
2420
  addMemory,
2338
2421
  canRead,
@@ -2347,8 +2430,11 @@ function isPlausibleUpdateTarget(newEmbedding, targetEmbedding, minSimilarity) {
2347
2430
  encode,
2348
2431
  ensureCollection,
2349
2432
  generateReviewBatch,
2433
+ getEmbeddingDevice,
2350
2434
  getEmbeddingDim,
2435
+ getEmbeddingDtype,
2351
2436
  getEmbeddingModel,
2437
+ getEmbeddingPooling,
2352
2438
  getNote,
2353
2439
  invalidateNote,
2354
2440
  isModelLoaded,