openclaw-amem 1.4.1 → 1.4.3
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.js +104 -22
- package/openclaw.plugin.json +4 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -56,41 +56,66 @@ var init_config = __esm({
|
|
|
56
56
|
function getEmbeddingModel() {
|
|
57
57
|
return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
58
58
|
}
|
|
59
|
+
function getEmbeddingPooling() {
|
|
60
|
+
const explicit = process.env.AMEM_EMBED_POOLING?.trim().toLowerCase();
|
|
61
|
+
if (explicit === "mean" || explicit === "cls") return explicit;
|
|
62
|
+
const basename2 = getEmbeddingModel().split("/").pop()?.toLowerCase() ?? "";
|
|
63
|
+
return CLS_POOLED_MODELS.has(basename2) ? "cls" : "mean";
|
|
64
|
+
}
|
|
65
|
+
function getEmbeddingDevice() {
|
|
66
|
+
return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
|
|
67
|
+
}
|
|
68
|
+
function getEmbeddingDtype() {
|
|
69
|
+
return process.env.AMEM_EMBED_DTYPE?.trim() || void 0;
|
|
70
|
+
}
|
|
71
|
+
function extractorKey() {
|
|
72
|
+
return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
|
|
73
|
+
}
|
|
59
74
|
async function getExtractor() {
|
|
60
|
-
const wanted =
|
|
61
|
-
if (extractor &&
|
|
75
|
+
const wanted = extractorKey();
|
|
76
|
+
if (extractor && loadedKey === wanted) return extractor;
|
|
62
77
|
if (!pipeline) {
|
|
63
78
|
const mod = await import("@huggingface/transformers");
|
|
64
79
|
pipeline = mod.pipeline;
|
|
65
80
|
}
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
const device = getEmbeddingDevice();
|
|
82
|
+
const dtype = getEmbeddingDtype();
|
|
83
|
+
extractor = await pipeline("feature-extraction", getEmbeddingModel(), {
|
|
84
|
+
revision: "main",
|
|
85
|
+
// Omitted entirely when unset, so an unconfigured install gets exactly the
|
|
86
|
+
// library defaults it got before these existed.
|
|
87
|
+
...device ? { device } : {},
|
|
88
|
+
...dtype ? { dtype } : {}
|
|
68
89
|
});
|
|
69
|
-
|
|
90
|
+
loadedKey = wanted;
|
|
70
91
|
cachedDim = null;
|
|
71
92
|
return extractor;
|
|
72
93
|
}
|
|
73
94
|
async function getEmbeddingDim() {
|
|
74
|
-
if (cachedDim !== null &&
|
|
95
|
+
if (cachedDim !== null && loadedKey === extractorKey()) return cachedDim;
|
|
75
96
|
const probe = await encode("dimension probe");
|
|
76
97
|
cachedDim = probe.length;
|
|
77
98
|
return cachedDim;
|
|
78
99
|
}
|
|
79
|
-
function
|
|
100
|
+
function poolNormalize(output, attentionMask, mode) {
|
|
80
101
|
const seqLen = output.length;
|
|
81
102
|
const dim = output[0].length;
|
|
82
103
|
const pooled = new Array(dim).fill(0);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
maskSum
|
|
104
|
+
if (mode === "cls") {
|
|
105
|
+
for (let j = 0; j < dim; j++) pooled[j] = output[0][j];
|
|
106
|
+
} else {
|
|
107
|
+
let maskSum = 0;
|
|
108
|
+
for (let i = 0; i < seqLen; i++) {
|
|
109
|
+
const m = attentionMask[i];
|
|
110
|
+
maskSum += m;
|
|
111
|
+
for (let j = 0; j < dim; j++) {
|
|
112
|
+
pooled[j] += output[i][j] * m;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
87
115
|
for (let j = 0; j < dim; j++) {
|
|
88
|
-
pooled[j]
|
|
116
|
+
pooled[j] /= Math.max(maskSum, 1e-9);
|
|
89
117
|
}
|
|
90
118
|
}
|
|
91
|
-
for (let j = 0; j < dim; j++) {
|
|
92
|
-
pooled[j] /= Math.max(maskSum, 1e-9);
|
|
93
|
-
}
|
|
94
119
|
let norm = 0;
|
|
95
120
|
for (const v of pooled) norm += v * v;
|
|
96
121
|
norm = Math.sqrt(norm);
|
|
@@ -98,7 +123,8 @@ function meanPoolingNormalize(output, attentionMask) {
|
|
|
98
123
|
}
|
|
99
124
|
async function encode(text) {
|
|
100
125
|
const ext = await getExtractor();
|
|
101
|
-
const
|
|
126
|
+
const pooling = getEmbeddingPooling();
|
|
127
|
+
const result = await ext(text, { pooling, normalize: true });
|
|
102
128
|
if (result && result.data) {
|
|
103
129
|
return Array.from(result.data);
|
|
104
130
|
}
|
|
@@ -114,7 +140,7 @@ async function encode(text) {
|
|
|
114
140
|
}
|
|
115
141
|
raw.push(row);
|
|
116
142
|
}
|
|
117
|
-
return
|
|
143
|
+
return poolNormalize(raw, new Array(seqLen).fill(1), pooling);
|
|
118
144
|
}
|
|
119
145
|
throw new Error("Unexpected embedding output shape");
|
|
120
146
|
}
|
|
@@ -129,15 +155,28 @@ function cosineSimilarity(a, b) {
|
|
|
129
155
|
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
130
156
|
return dot;
|
|
131
157
|
}
|
|
132
|
-
var pipeline, extractor,
|
|
158
|
+
var pipeline, extractor, loadedKey, cachedDim, DEFAULT_EMBEDDING_MODEL, CLS_POOLED_MODELS;
|
|
133
159
|
var init_embedding = __esm({
|
|
134
160
|
"../amem-core/src/embedding.ts"() {
|
|
135
161
|
"use strict";
|
|
136
162
|
pipeline = null;
|
|
137
163
|
extractor = null;
|
|
138
|
-
|
|
164
|
+
loadedKey = null;
|
|
139
165
|
cachedDim = null;
|
|
140
166
|
DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
167
|
+
CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
|
|
168
|
+
"bge-m3",
|
|
169
|
+
"bge-base-zh-v1.5",
|
|
170
|
+
"bge-small-zh-v1.5",
|
|
171
|
+
"bge-base-en-v1.5",
|
|
172
|
+
"bge-small-en-v1.5",
|
|
173
|
+
"bge-large-en-v1.5",
|
|
174
|
+
"gte-multilingual-base",
|
|
175
|
+
"gte-modernbert-base",
|
|
176
|
+
"gte-large-en-v1.5",
|
|
177
|
+
"snowflake-arctic-embed-m",
|
|
178
|
+
"snowflake-arctic-embed-l"
|
|
179
|
+
]);
|
|
141
180
|
}
|
|
142
181
|
});
|
|
143
182
|
|
|
@@ -155,6 +194,20 @@ var init_auth = __esm({
|
|
|
155
194
|
});
|
|
156
195
|
|
|
157
196
|
// ../amem-core/src/storage.ts
|
|
197
|
+
function migrationHint(collection, targetModel) {
|
|
198
|
+
return `migrate to a new collection:
|
|
199
|
+
|
|
200
|
+
AMEM_EMBED_MODEL=${targetModel} \\
|
|
201
|
+
npx --package=@amemhq/core amem-migrate --to ${collection}_v2
|
|
202
|
+
|
|
203
|
+
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.`;
|
|
204
|
+
}
|
|
205
|
+
async function recordCollectionModel(collection, model) {
|
|
206
|
+
try {
|
|
207
|
+
await qdrant("PATCH", `/collections/${collection}`, { metadata: { embedding_model: model } });
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
158
211
|
async function qdrant(method, path6, body) {
|
|
159
212
|
const res = await fetch(`${QDRANT_URL}${path6}`, {
|
|
160
213
|
method,
|
|
@@ -195,6 +248,14 @@ async function ensureCollection(collectionName) {
|
|
|
195
248
|
throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
|
|
196
249
|
}
|
|
197
250
|
}
|
|
251
|
+
const recorded = existing.config?.metadata?.embedding_model;
|
|
252
|
+
const current = getEmbeddingModel();
|
|
253
|
+
if (typeof recorded === "string" && recorded !== current) {
|
|
254
|
+
throw new EmbeddingModelMismatchError(col, recorded, current);
|
|
255
|
+
}
|
|
256
|
+
if (recorded === void 0) {
|
|
257
|
+
await recordCollectionModel(col, current);
|
|
258
|
+
}
|
|
198
259
|
markReady();
|
|
199
260
|
return;
|
|
200
261
|
}
|
|
@@ -206,6 +267,7 @@ async function ensureCollection(collectionName) {
|
|
|
206
267
|
} catch (err) {
|
|
207
268
|
if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
|
|
208
269
|
}
|
|
270
|
+
await recordCollectionModel(col, getEmbeddingModel());
|
|
209
271
|
await qdrant("PUT", `/collections/${col}/index`, {
|
|
210
272
|
field_name: "agent_id",
|
|
211
273
|
field_schema: "keyword"
|
|
@@ -251,6 +313,7 @@ async function collectionDimRaw(collection) {
|
|
|
251
313
|
}
|
|
252
314
|
async function createCollectionRaw(collection, size) {
|
|
253
315
|
await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
|
|
316
|
+
await recordCollectionModel(collection, getEmbeddingModel());
|
|
254
317
|
for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
|
|
255
318
|
await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
|
|
256
319
|
}
|
|
@@ -643,7 +706,7 @@ async function invalidateNote(id, callerAgentId) {
|
|
|
643
706
|
async function patchNotePayload(id, fields) {
|
|
644
707
|
return makeCrud(getCollection()).patchNotePayload(id, fields);
|
|
645
708
|
}
|
|
646
|
-
var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, _collectionReady, _collectionReadyMap;
|
|
709
|
+
var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, EmbeddingModelMismatchError, _collectionReady, _collectionReadyMap;
|
|
647
710
|
var init_storage = __esm({
|
|
648
711
|
"../amem-core/src/storage.ts"() {
|
|
649
712
|
"use strict";
|
|
@@ -655,7 +718,7 @@ var init_storage = __esm({
|
|
|
655
718
|
constructor(collection, collectionDim, modelDim, model) {
|
|
656
719
|
super(
|
|
657
720
|
`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.
|
|
658
|
-
Either set AMEM_EMBED_MODEL back to the model this collection was built with, or
|
|
721
|
+
Either set AMEM_EMBED_MODEL back to the model this collection was built with, or ${migrationHint(collection, model)}`
|
|
659
722
|
);
|
|
660
723
|
this.collection = collection;
|
|
661
724
|
this.collectionDim = collectionDim;
|
|
@@ -668,6 +731,21 @@ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or
|
|
|
668
731
|
modelDim;
|
|
669
732
|
model;
|
|
670
733
|
};
|
|
734
|
+
EmbeddingModelMismatchError = class extends Error {
|
|
735
|
+
constructor(collection, collectionModel, configuredModel) {
|
|
736
|
+
super(
|
|
737
|
+
`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.
|
|
738
|
+
Either set AMEM_EMBED_MODEL back to "${collectionModel}", or ` + migrationHint(collection, configuredModel)
|
|
739
|
+
);
|
|
740
|
+
this.collection = collection;
|
|
741
|
+
this.collectionModel = collectionModel;
|
|
742
|
+
this.configuredModel = configuredModel;
|
|
743
|
+
this.name = "EmbeddingModelMismatchError";
|
|
744
|
+
}
|
|
745
|
+
collection;
|
|
746
|
+
collectionModel;
|
|
747
|
+
configuredModel;
|
|
748
|
+
};
|
|
671
749
|
_collectionReady = false;
|
|
672
750
|
_collectionReadyMap = /* @__PURE__ */ new Map();
|
|
673
751
|
}
|
|
@@ -2379,6 +2457,7 @@ __export(src_exports, {
|
|
|
2379
2457
|
DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
|
|
2380
2458
|
DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
|
|
2381
2459
|
EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
|
|
2460
|
+
EmbeddingModelMismatchError: () => EmbeddingModelMismatchError,
|
|
2382
2461
|
addEpisodic: () => addEpisodic,
|
|
2383
2462
|
addMemory: () => addMemory,
|
|
2384
2463
|
canRead: () => canRead,
|
|
@@ -2393,8 +2472,11 @@ __export(src_exports, {
|
|
|
2393
2472
|
encode: () => encode,
|
|
2394
2473
|
ensureCollection: () => ensureCollection,
|
|
2395
2474
|
generateReviewBatch: () => generateReviewBatch,
|
|
2475
|
+
getEmbeddingDevice: () => getEmbeddingDevice,
|
|
2396
2476
|
getEmbeddingDim: () => getEmbeddingDim,
|
|
2477
|
+
getEmbeddingDtype: () => getEmbeddingDtype,
|
|
2397
2478
|
getEmbeddingModel: () => getEmbeddingModel,
|
|
2479
|
+
getEmbeddingPooling: () => getEmbeddingPooling,
|
|
2398
2480
|
getNote: () => getNote,
|
|
2399
2481
|
invalidateNote: () => invalidateNote,
|
|
2400
2482
|
isModelLoaded: () => isModelLoaded,
|
|
@@ -2527,7 +2609,7 @@ function register(api) {
|
|
|
2527
2609
|
`openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
|
|
2528
2610
|
);
|
|
2529
2611
|
ensureCollection(pluginConfig.collection).catch((e) => {
|
|
2530
|
-
if (e instanceof EmbeddingDimensionMismatchError) {
|
|
2612
|
+
if (e instanceof EmbeddingDimensionMismatchError || e instanceof EmbeddingModelMismatchError) {
|
|
2531
2613
|
logger.error(`openclaw-amem: memory is UNUSABLE \u2014 ${e.message}`);
|
|
2532
2614
|
} else {
|
|
2533
2615
|
logger.warn(`openclaw-amem: ensureCollection failed \u2014 ${e.message}`);
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-amem",
|
|
3
3
|
"name": "amem",
|
|
4
4
|
"description": "Catches memories that contradict each other. Notes rewrite themselves as new ones arrive, link into a graph, and stay separated per agent and per person. Runs on a small model. Requires a local Qdrant. 中文走 jieba 分词,提示词有中文版。",
|
|
5
|
-
"version": "1.4.
|
|
5
|
+
"version": "1.4.3",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"openclaw": {
|
|
8
8
|
"compat": {
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
"AMEM_LLM_TIMEOUT",
|
|
40
40
|
"AMEM_CRUD_UPDATE_MIN_SIM",
|
|
41
41
|
"AMEM_EMBED_MODEL",
|
|
42
|
+
"AMEM_EMBED_POOLING",
|
|
43
|
+
"AMEM_EMBED_DEVICE",
|
|
44
|
+
"AMEM_EMBED_DTYPE",
|
|
42
45
|
"AMEM_COLLECTION",
|
|
43
46
|
"AMEM_DATA_DIR",
|
|
44
47
|
"AMEM_EVO_COUNTER_PATH",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-amem",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Catches memories that contradict each other. Notes rewrite themselves as new ones arrive, link into a graph, and stay separated per agent and per person. Runs on a small model. Requires a local Qdrant. 中文走 jieba 分词,提示词有中文版。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@anthropic-ai/sdk": "^0.112.
|
|
19
|
+
"@anthropic-ai/sdk": "^0.112.5",
|
|
20
20
|
"@huggingface/transformers": "^4.2.0",
|
|
21
21
|
"@node-rs/jieba": "^2.0.1",
|
|
22
22
|
"@qdrant/js-client-rest": "^1.18.0",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/node": "^26.1.1",
|
|
29
29
|
"@types/uuid": "^11.0.0",
|
|
30
30
|
"eslint": "^10.7.0",
|
|
31
|
-
"prettier": "^3.9.
|
|
31
|
+
"prettier": "^3.9.6",
|
|
32
32
|
"tsup": "^8.4.0",
|
|
33
33
|
"tsx": "^4.23.1",
|
|
34
34
|
"typescript": "^6.0.3",
|