@amemhq/core 1.0.1 → 2.0.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/README.md +2 -2
- package/dist/chunk-K6WZTDM7.js +2353 -0
- package/dist/chunk-K6WZTDM7.js.map +1 -0
- package/dist/cli-migrate.cjs +1086 -0
- package/dist/cli-migrate.cjs.map +1 -0
- package/dist/cli-migrate.d.cts +30 -0
- package/dist/cli-migrate.d.ts +30 -0
- package/dist/cli-migrate.js +147 -0
- package/dist/cli-migrate.js.map +1 -0
- package/dist/index.cjs +289 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -28
- package/dist/index.d.ts +181 -28
- package/dist/index.js +82 -2120
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli-migrate.ts
|
|
32
|
+
var cli_migrate_exports = {};
|
|
33
|
+
__export(cli_migrate_exports, {
|
|
34
|
+
carried: () => carried,
|
|
35
|
+
deriveTarget: () => deriveTarget,
|
|
36
|
+
parseArgs: () => parseArgs
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(cli_migrate_exports);
|
|
39
|
+
|
|
40
|
+
// src/embedding.ts
|
|
41
|
+
var pipeline = null;
|
|
42
|
+
var extractor = null;
|
|
43
|
+
var loadedKey = null;
|
|
44
|
+
var cachedDim = null;
|
|
45
|
+
var DEFAULT_EMBEDDING_MODEL = "Xenova/bge-m3";
|
|
46
|
+
var DEFAULT_MODEL_DTYPE = "fp16";
|
|
47
|
+
var pinnedModel = null;
|
|
48
|
+
function getEmbeddingModel() {
|
|
49
|
+
return process.env.AMEM_EMBED_MODEL?.trim() || pinnedModel || DEFAULT_EMBEDDING_MODEL;
|
|
50
|
+
}
|
|
51
|
+
var CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
|
|
52
|
+
"bge-m3",
|
|
53
|
+
"bge-base-zh-v1.5",
|
|
54
|
+
"bge-small-zh-v1.5",
|
|
55
|
+
"bge-base-en-v1.5",
|
|
56
|
+
"bge-small-en-v1.5",
|
|
57
|
+
"bge-large-en-v1.5",
|
|
58
|
+
"gte-multilingual-base",
|
|
59
|
+
"gte-modernbert-base",
|
|
60
|
+
"gte-large-en-v1.5",
|
|
61
|
+
"snowflake-arctic-embed-m",
|
|
62
|
+
"snowflake-arctic-embed-l"
|
|
63
|
+
]);
|
|
64
|
+
function getEmbeddingPooling() {
|
|
65
|
+
const explicit = process.env.AMEM_EMBED_POOLING?.trim().toLowerCase();
|
|
66
|
+
if (explicit === "mean" || explicit === "cls") return explicit;
|
|
67
|
+
const basename = getEmbeddingModel().split("/").pop()?.toLowerCase() ?? "";
|
|
68
|
+
return CLS_POOLED_MODELS.has(basename) ? "cls" : "mean";
|
|
69
|
+
}
|
|
70
|
+
function getEmbeddingDevice() {
|
|
71
|
+
return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
|
|
72
|
+
}
|
|
73
|
+
function getEmbeddingDtype() {
|
|
74
|
+
const explicit = process.env.AMEM_EMBED_DTYPE?.trim();
|
|
75
|
+
if (explicit) return explicit;
|
|
76
|
+
return getEmbeddingModel() === DEFAULT_EMBEDDING_MODEL ? DEFAULT_MODEL_DTYPE : void 0;
|
|
77
|
+
}
|
|
78
|
+
function extractorKey() {
|
|
79
|
+
return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
|
|
80
|
+
}
|
|
81
|
+
async function getExtractor() {
|
|
82
|
+
const wanted = extractorKey();
|
|
83
|
+
if (extractor && loadedKey === wanted) return extractor;
|
|
84
|
+
if (!pipeline) {
|
|
85
|
+
const mod = await import("@huggingface/transformers");
|
|
86
|
+
pipeline = mod.pipeline;
|
|
87
|
+
}
|
|
88
|
+
const device = getEmbeddingDevice();
|
|
89
|
+
const dtype = getEmbeddingDtype();
|
|
90
|
+
extractor = await pipeline("feature-extraction", getEmbeddingModel(), {
|
|
91
|
+
revision: "main",
|
|
92
|
+
// Omitted entirely when unset, so an unconfigured install gets exactly the
|
|
93
|
+
// library defaults it got before these existed.
|
|
94
|
+
...device ? { device } : {},
|
|
95
|
+
...dtype ? { dtype } : {}
|
|
96
|
+
});
|
|
97
|
+
loadedKey = wanted;
|
|
98
|
+
cachedDim = null;
|
|
99
|
+
return extractor;
|
|
100
|
+
}
|
|
101
|
+
async function getEmbeddingDim() {
|
|
102
|
+
if (cachedDim !== null && loadedKey === extractorKey()) return cachedDim;
|
|
103
|
+
const probe = await encode("dimension probe");
|
|
104
|
+
cachedDim = probe.length;
|
|
105
|
+
return cachedDim;
|
|
106
|
+
}
|
|
107
|
+
function poolNormalize(output, attentionMask, mode) {
|
|
108
|
+
const seqLen = output.length;
|
|
109
|
+
const dim = output[0].length;
|
|
110
|
+
const pooled = new Array(dim).fill(0);
|
|
111
|
+
if (mode === "cls") {
|
|
112
|
+
for (let j = 0; j < dim; j++) pooled[j] = output[0][j];
|
|
113
|
+
} else {
|
|
114
|
+
let maskSum = 0;
|
|
115
|
+
for (let i = 0; i < seqLen; i++) {
|
|
116
|
+
const m = attentionMask[i];
|
|
117
|
+
maskSum += m;
|
|
118
|
+
for (let j = 0; j < dim; j++) {
|
|
119
|
+
pooled[j] += output[i][j] * m;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (let j = 0; j < dim; j++) {
|
|
123
|
+
pooled[j] /= Math.max(maskSum, 1e-9);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let norm = 0;
|
|
127
|
+
for (const v of pooled) norm += v * v;
|
|
128
|
+
norm = Math.sqrt(norm);
|
|
129
|
+
return pooled.map((v) => v / Math.max(norm, 1e-9));
|
|
130
|
+
}
|
|
131
|
+
async function encode(text) {
|
|
132
|
+
const ext = await getExtractor();
|
|
133
|
+
const pooling = getEmbeddingPooling();
|
|
134
|
+
const result = await ext(text, { pooling, normalize: true });
|
|
135
|
+
if (result && result.data) {
|
|
136
|
+
return Array.from(result.data);
|
|
137
|
+
}
|
|
138
|
+
const tensor = result;
|
|
139
|
+
if (tensor.dims && tensor.dims.length === 3) {
|
|
140
|
+
const seqLen = tensor.dims[1];
|
|
141
|
+
const dim = tensor.dims[2];
|
|
142
|
+
const raw = [];
|
|
143
|
+
for (let i = 0; i < seqLen; i++) {
|
|
144
|
+
const row = [];
|
|
145
|
+
for (let j = 0; j < dim; j++) {
|
|
146
|
+
row.push(tensor.data[i * dim + j]);
|
|
147
|
+
}
|
|
148
|
+
raw.push(row);
|
|
149
|
+
}
|
|
150
|
+
return poolNormalize(raw, new Array(seqLen).fill(1), pooling);
|
|
151
|
+
}
|
|
152
|
+
throw new Error("Unexpected embedding output shape");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/storage.ts
|
|
156
|
+
var QDRANT_URL = "http://localhost:6333";
|
|
157
|
+
var getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
|
|
158
|
+
async function recordCollectionModel(collection, model) {
|
|
159
|
+
try {
|
|
160
|
+
await qdrant("PATCH", `/collections/${collection}`, { metadata: { embedding_model: model } });
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function qdrant(method, path2, body) {
|
|
165
|
+
const res = await fetch(`${QDRANT_URL}${path2}`, {
|
|
166
|
+
method,
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
body: body ? JSON.stringify(body) : void 0
|
|
169
|
+
});
|
|
170
|
+
const data = await res.json();
|
|
171
|
+
if (!res.ok || data.status && data.status !== "ok" && data.status !== "acknowledged") {
|
|
172
|
+
throw new Error(`Qdrant ${method} ${path2} failed: ${data.error || JSON.stringify(data)}`);
|
|
173
|
+
}
|
|
174
|
+
return data.result;
|
|
175
|
+
}
|
|
176
|
+
async function scrollAllRaw(collection, limit = 1e4) {
|
|
177
|
+
const out = [];
|
|
178
|
+
let offset = void 0;
|
|
179
|
+
for (; ; ) {
|
|
180
|
+
const body = { with_payload: true, with_vector: true, limit };
|
|
181
|
+
if (offset !== void 0 && offset !== null) body.offset = offset;
|
|
182
|
+
const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
|
|
183
|
+
out.push(...res.points);
|
|
184
|
+
offset = res.next_page_offset;
|
|
185
|
+
if (offset === void 0 || offset === null || res.points.length === 0) break;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
async function countPointsRaw(collection) {
|
|
190
|
+
const res = await qdrant("POST", `/collections/${collection}/points/count`, { exact: true });
|
|
191
|
+
return res.count;
|
|
192
|
+
}
|
|
193
|
+
async function collectionDimRaw(collection) {
|
|
194
|
+
try {
|
|
195
|
+
const info = await qdrant("GET", `/collections/${collection}`);
|
|
196
|
+
return info.config?.params?.vectors?.size ?? null;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function createCollectionRaw(collection, size) {
|
|
202
|
+
await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
|
|
203
|
+
await recordCollectionModel(collection, getEmbeddingModel());
|
|
204
|
+
for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
|
|
205
|
+
await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function upsertPointsRaw(collection, points) {
|
|
209
|
+
await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
|
|
210
|
+
}
|
|
211
|
+
async function scrollIdsRaw(collection, limit = 1e4) {
|
|
212
|
+
const ids = /* @__PURE__ */ new Set();
|
|
213
|
+
let offset = void 0;
|
|
214
|
+
for (; ; ) {
|
|
215
|
+
const body = { with_payload: false, with_vector: false, limit };
|
|
216
|
+
if (offset !== void 0 && offset !== null) body.offset = offset;
|
|
217
|
+
const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
|
|
218
|
+
for (const p of res.points) ids.add(String(p.id));
|
|
219
|
+
offset = res.next_page_offset;
|
|
220
|
+
if (offset === void 0 || offset === null || res.points.length === 0) break;
|
|
221
|
+
}
|
|
222
|
+
return ids;
|
|
223
|
+
}
|
|
224
|
+
async function deleteCollectionRaw(collection) {
|
|
225
|
+
await qdrant("DELETE", `/collections/${collection}`);
|
|
226
|
+
}
|
|
227
|
+
async function resolveAliasRaw(alias) {
|
|
228
|
+
try {
|
|
229
|
+
const res = await qdrant("GET", `/aliases`);
|
|
230
|
+
return res.aliases.find((a) => a.alias_name === alias)?.collection_name ?? null;
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function createAliasRaw(alias, collection) {
|
|
236
|
+
await qdrant("POST", `/collections/aliases`, {
|
|
237
|
+
actions: [{ create_alias: { collection_name: collection, alias_name: alias } }]
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function setAliasRaw(alias, collection) {
|
|
241
|
+
await qdrant("POST", `/collections/aliases`, {
|
|
242
|
+
actions: [
|
|
243
|
+
{ delete_alias: { alias_name: alias } },
|
|
244
|
+
{ create_alias: { collection_name: collection, alias_name: alias } }
|
|
245
|
+
]
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function noteToPoint(note) {
|
|
249
|
+
return {
|
|
250
|
+
id: note.id,
|
|
251
|
+
vector: note.embedding,
|
|
252
|
+
payload: {
|
|
253
|
+
content: note.content,
|
|
254
|
+
keywords: note.keywords,
|
|
255
|
+
tags: note.tags,
|
|
256
|
+
context: note.context,
|
|
257
|
+
links: note.links,
|
|
258
|
+
timestamp: note.timestamp,
|
|
259
|
+
agent_id: note.agent_id,
|
|
260
|
+
hash: note.hash,
|
|
261
|
+
// 13-A
|
|
262
|
+
retrieval_count: note.retrieval_count ?? 0,
|
|
263
|
+
last_accessed: note.last_accessed || note.timestamp,
|
|
264
|
+
// 13-B: stored as JSON string (Qdrant payload can't handle nested array-of-objects)
|
|
265
|
+
evolution_history: JSON.stringify(note.evolution_history ?? []),
|
|
266
|
+
// 13-E
|
|
267
|
+
category: note.category || "General",
|
|
268
|
+
is_active: note.is_active !== false,
|
|
269
|
+
// 26B
|
|
270
|
+
topics: note.topics ?? [],
|
|
271
|
+
// 26A
|
|
272
|
+
note_type: note.note_type || "memory",
|
|
273
|
+
// 29
|
|
274
|
+
pending_merge: note.pending_merge ?? false,
|
|
275
|
+
// 30
|
|
276
|
+
evolution_type: note.evolution_type || "",
|
|
277
|
+
conflict: note.conflict ?? false,
|
|
278
|
+
conflicts_with: note.conflicts_with ?? [],
|
|
279
|
+
conflict_reason: note.conflict_reason ?? "",
|
|
280
|
+
conflict_scanned_at: note.conflict_scanned_at ?? "",
|
|
281
|
+
subjects: note.subjects ?? [],
|
|
282
|
+
// 31
|
|
283
|
+
ephemeral: note.ephemeral ?? false,
|
|
284
|
+
low_quality: note.low_quality ?? false,
|
|
285
|
+
// 32
|
|
286
|
+
owner: note.owner || note.agent_id,
|
|
287
|
+
readers: note.readers ?? [note.agent_id],
|
|
288
|
+
writers: note.writers ?? [note.agent_id]
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function pointToNote(point) {
|
|
293
|
+
const p = point.payload;
|
|
294
|
+
const timestamp = p.timestamp || "";
|
|
295
|
+
let evolutionHistory = [];
|
|
296
|
+
try {
|
|
297
|
+
const raw = p.evolution_history;
|
|
298
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
299
|
+
evolutionHistory = JSON.parse(raw);
|
|
300
|
+
} else if (Array.isArray(raw)) {
|
|
301
|
+
evolutionHistory = raw;
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
evolutionHistory = [];
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
id: String(point.id),
|
|
308
|
+
content: p.content || "",
|
|
309
|
+
keywords: p.keywords || [],
|
|
310
|
+
tags: p.tags || [],
|
|
311
|
+
context: p.context || "",
|
|
312
|
+
links: p.links || [],
|
|
313
|
+
timestamp,
|
|
314
|
+
agent_id: p.agent_id || "main",
|
|
315
|
+
embedding: point.vector || [],
|
|
316
|
+
hash: p.hash || "",
|
|
317
|
+
// 13-A
|
|
318
|
+
retrieval_count: typeof p.retrieval_count === "number" ? p.retrieval_count : 0,
|
|
319
|
+
last_accessed: p.last_accessed || timestamp,
|
|
320
|
+
// 13-B
|
|
321
|
+
evolution_history: evolutionHistory,
|
|
322
|
+
// 13-E
|
|
323
|
+
category: p.category || "General",
|
|
324
|
+
is_active: p.is_active !== false,
|
|
325
|
+
// 26A
|
|
326
|
+
note_type: p.note_type === "knowledge" ? "knowledge" : "memory",
|
|
327
|
+
// 26B
|
|
328
|
+
topics: Array.isArray(p.topics) ? p.topics : [],
|
|
329
|
+
// 29
|
|
330
|
+
pending_merge: p.pending_merge === true,
|
|
331
|
+
// 30
|
|
332
|
+
evolution_type: typeof p.evolution_type === "string" && ["EVOLVE", "CONFLICT", "EXPAND", "NEW"].includes(p.evolution_type) ? p.evolution_type : void 0,
|
|
333
|
+
conflict: p.conflict === true,
|
|
334
|
+
conflicts_with: Array.isArray(p.conflicts_with) ? p.conflicts_with.filter((v) => typeof v === "string") : [],
|
|
335
|
+
conflict_reason: typeof p.conflict_reason === "string" ? p.conflict_reason : "",
|
|
336
|
+
conflict_scanned_at: typeof p.conflict_scanned_at === "string" ? p.conflict_scanned_at : "",
|
|
337
|
+
subjects: Array.isArray(p.subjects) ? p.subjects.filter((v) => typeof v === "string") : [],
|
|
338
|
+
// 31
|
|
339
|
+
ephemeral: p.ephemeral === true,
|
|
340
|
+
low_quality: p.low_quality === true,
|
|
341
|
+
// 32
|
|
342
|
+
owner: p.owner || p.agent_id || "main",
|
|
343
|
+
readers: Array.isArray(p.readers) ? p.readers : [p.agent_id || "main"],
|
|
344
|
+
writers: Array.isArray(p.writers) ? p.writers : [p.agent_id || "main"]
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/llm.ts
|
|
349
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
350
|
+
var import_openai = __toESM(require("openai"), 1);
|
|
351
|
+
|
|
352
|
+
// src/prompts.ts
|
|
353
|
+
var LOCALE = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
|
|
354
|
+
var en = {
|
|
355
|
+
crudDecision: (userText, assistantText, memoryList) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.
|
|
356
|
+
|
|
357
|
+
## Conversation
|
|
358
|
+
|
|
359
|
+
User: ${userText}
|
|
360
|
+
Assistant: ${assistantText}
|
|
361
|
+
|
|
362
|
+
## Existing relevant memories (identified by integer idx)
|
|
363
|
+
|
|
364
|
+
${memoryList}
|
|
365
|
+
|
|
366
|
+
## Task
|
|
367
|
+
|
|
368
|
+
Extract only genuinely important long-term facts (decisions, preferences, account info, project status, key insights). Skip small talk, confirmations, and information already captured in existing memories.
|
|
369
|
+
|
|
370
|
+
## Operation types
|
|
371
|
+
- NEW: Extract a brand new fact not present in existing memories
|
|
372
|
+
- UPDATE: New information refines or supersedes an existing memory; specify existingIdx
|
|
373
|
+
- DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content
|
|
374
|
+
- NONE: Nothing worth recording, or information already fully captured
|
|
375
|
+
|
|
376
|
+
## Output format
|
|
377
|
+
|
|
378
|
+
Return a JSON array. Each item:
|
|
379
|
+
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "fact content", "existingIdx": integer or omit, "reason": "optional"}
|
|
380
|
+
|
|
381
|
+
Return at most 3 operations. If nothing is worth recording, return [].
|
|
382
|
+
Return only the JSON array, no other text.
|
|
383
|
+
|
|
384
|
+
Examples:
|
|
385
|
+
|
|
386
|
+
1. New preference:
|
|
387
|
+
[{"action": "NEW", "fact": "User prefers TypeScript over JavaScript", "reason": "Explicitly stated tech preference"}]
|
|
388
|
+
|
|
389
|
+
2. Updating an existing memory (idx 0 was "User is evaluating React and Vue"):
|
|
390
|
+
[{"action": "UPDATE", "fact": "User decided to use React (dropped Vue)", "existingIdx": 0, "reason": "Decision finalized, update evaluation status"}]
|
|
391
|
+
|
|
392
|
+
3. Conversation is just "Sure, thanks" / "Got it" with no new info:
|
|
393
|
+
[]`,
|
|
394
|
+
shouldMerge: (contentA, contentB) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.
|
|
395
|
+
|
|
396
|
+
Memory A: ${contentA}
|
|
397
|
+
Memory B: ${contentB}
|
|
398
|
+
|
|
399
|
+
Rules:
|
|
400
|
+
- If both memories express the same core fact (possibly different wording or granularity), return:
|
|
401
|
+
{"shouldMerge": true, "merged": "Concise merged statement preserving key details from both, more complete than either alone"}
|
|
402
|
+
- If the memories are complementary, on different topics, or contain different specific facts, return:
|
|
403
|
+
{"shouldMerge": false}
|
|
404
|
+
|
|
405
|
+
Return only JSON, no other text.
|
|
406
|
+
|
|
407
|
+
Examples:
|
|
408
|
+
|
|
409
|
+
1. Should merge (different granularity):
|
|
410
|
+
A: "Project uses PostgreSQL"
|
|
411
|
+
B: "Project's primary database is PostgreSQL 16, deployed on AWS RDS"
|
|
412
|
+
-> {"shouldMerge": true, "merged": "Project uses PostgreSQL 16 as primary database, deployed on AWS RDS"}
|
|
413
|
+
|
|
414
|
+
2. Should NOT merge (complementary but distinct):
|
|
415
|
+
A: "User prefers VS Code"
|
|
416
|
+
B: "User's VS Code uses One Dark Pro theme"
|
|
417
|
+
-> {"shouldMerge": false}`,
|
|
418
|
+
evolutionJudge: (oldContent, newContent) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.
|
|
419
|
+
|
|
420
|
+
Old memory: ${oldContent}
|
|
421
|
+
New memory: ${newContent}
|
|
422
|
+
|
|
423
|
+
Classification rules:
|
|
424
|
+
|
|
425
|
+
- EVOLVE: New content deepens or updates the old memory (e.g. "Considering Next.js" -> "Decided on Next.js 14 App Router")
|
|
426
|
+
Return: {"type": "EVOLVE", "mergedContent": "Merged content preserving the evolution trajectory"}
|
|
427
|
+
|
|
428
|
+
- CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. "Uses MySQL as primary DB" vs "Migrated to PostgreSQL")
|
|
429
|
+
Return: {"type": "CONFLICT"}
|
|
430
|
+
|
|
431
|
+
- EXPAND: New information supplements the old memory on the same topic (e.g. "Handles backend dev" + "Backend uses Go and gRPC")
|
|
432
|
+
Return: {"type": "EXPAND", "mergedContent": "Merged content integrating both pieces of information"}
|
|
433
|
+
|
|
434
|
+
- NEW: Completely unrelated information, no substantive connection to the old memory
|
|
435
|
+
Return: {"type": "NEW"}
|
|
436
|
+
|
|
437
|
+
Return only JSON, no other text.`,
|
|
438
|
+
conflictScan: (numberedNotes) => `You are auditing a person's memory store for CONTRADICTIONS.
|
|
439
|
+
|
|
440
|
+
Below are numbered memories. Find pairs that CANNOT both be true of the same person at the same time.
|
|
441
|
+
|
|
442
|
+
${numberedNotes}
|
|
443
|
+
|
|
444
|
+
What counts as a contradiction:
|
|
445
|
+
- The same attribute holding two incompatible values ("lives in Paris" vs "moved to Berlin")
|
|
446
|
+
- A stated preference or constraint that a later memory violates ("is vegetarian" vs "loved the steak")
|
|
447
|
+
- A fact that a later memory supersedes ("uses MySQL" vs "migrated to PostgreSQL")
|
|
448
|
+
|
|
449
|
+
What does NOT count \u2014 be strict, these are the common false positives:
|
|
450
|
+
- Additive facts. Two things can both be true ("has a dog named Buddy" + "adopted a second dog, Scout" is NOT a contradiction)
|
|
451
|
+
- Change over time that both memories already acknowledge
|
|
452
|
+
- Merely similar or related topics
|
|
453
|
+
- Different contexts (likes coffee at work, tea at home)
|
|
454
|
+
|
|
455
|
+
For each contradicting pair, also say which one is SUPERSEDED \u2014 the one that is
|
|
456
|
+
no longer true. Judge this from the WORDING, not from any assumed order: phrases
|
|
457
|
+
like "used to", "back in 2019", "moved last month", "switched to" tell you which
|
|
458
|
+
statement describes the past. The memories are NOT listed in chronological order,
|
|
459
|
+
and the number does not imply age.
|
|
460
|
+
|
|
461
|
+
If you cannot tell which one is superseded, set it to null. That is a normal and
|
|
462
|
+
useful answer \u2014 say null rather than guessing, because a wrong guess retires a
|
|
463
|
+
memory that is still true.
|
|
464
|
+
|
|
465
|
+
Return ONLY a JSON array. Empty array if nothing genuinely contradicts:
|
|
466
|
+
[{"a": 0, "b": 3, "superseded": 0, "reason": "one short sentence naming the incompatible attribute"}]
|
|
467
|
+
|
|
468
|
+
"superseded" must be either the value of "a", the value of "b", or null.
|
|
469
|
+
Use the numbers shown. Report a pair once. Prefer returning nothing over guessing.`
|
|
470
|
+
};
|
|
471
|
+
var zh = {
|
|
472
|
+
crudDecision: (userText, assistantText, memoryList) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u7BA1\u7406 agent\uFF0C\u8D1F\u8D23\u5206\u6790\u5BF9\u8BDD\u5185\u5BB9\u5E76\u51B3\u5B9A\u5982\u4F55\u64CD\u4F5C\u8BB0\u5FC6\u5E93\u3002
|
|
473
|
+
|
|
474
|
+
## \u5BF9\u8BDD\u5185\u5BB9
|
|
475
|
+
|
|
476
|
+
\u7528\u6237\uFF1A${userText}
|
|
477
|
+
\u52A9\u624B\uFF1A${assistantText}
|
|
478
|
+
|
|
479
|
+
## \u5DF2\u6709\u76F8\u5173\u8BB0\u5FC6\uFF08\u7528\u6574\u6570 idx \u6807\u8BC6\uFF09
|
|
480
|
+
|
|
481
|
+
${memoryList}
|
|
482
|
+
|
|
483
|
+
## \u4EFB\u52A1
|
|
484
|
+
|
|
485
|
+
\u5206\u6790\u4E0A\u8FF0\u5BF9\u8BDD\uFF0C\u51B3\u5B9A\u9700\u8981\u54EA\u4E9B\u8BB0\u5FC6\u64CD\u4F5C\u3002\u53EA\u63D0\u53D6\u771F\u6B63\u91CD\u8981\u7684\u957F\u671F\u4E8B\u5B9E\uFF08\u51B3\u7B56\u3001\u504F\u597D\u3001\u8D26\u53F7\u4FE1\u606F\u3001\u9879\u76EE\u72B6\u6001\u3001\u5173\u952E\u6D1E\u5BDF\uFF09\u3002\u8DF3\u8FC7\u95F2\u804A\u3001\u786E\u8BA4\u8BED\u3001\u91CD\u590D\u4FE1\u606F\u3002
|
|
486
|
+
|
|
487
|
+
## \u64CD\u4F5C\u7C7B\u578B
|
|
488
|
+
- NEW\uFF1A\u63D0\u53D6\u5168\u65B0\u4E8B\u5B9E\uFF08\u5DF2\u6709\u8BB0\u5FC6\u4E2D\u6CA1\u6709\u7684\u4FE1\u606F\uFF09
|
|
489
|
+
- UPDATE\uFF1A\u65B0\u4FE1\u606F\u66F4\u65B0\u4E86\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\uFF0C\u7528 existingIdx \u6307\u5B9A\u8981\u66F4\u65B0\u7684\u6761\u76EE
|
|
490
|
+
- DELETE\uFF1A\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\u5DF2\u7ECF\u8FC7\u65F6\u3001\u53D1\u751F\u51B2\u7A81\u6216\u9519\u8BEF\uFF0C\u7528 existingIdx \u6307\u5B9A\uFF0Cfact \u586B\u539F\u5185\u5BB9
|
|
491
|
+
- NONE\uFF1A\u4E0D\u503C\u5F97\u8BB0\u5F55\u6216\u5DF2\u6709\u5B8C\u5168\u76F8\u540C\u7684\u4FE1\u606F
|
|
492
|
+
|
|
493
|
+
## \u8F93\u51FA\u683C\u5F0F
|
|
494
|
+
|
|
495
|
+
\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u6BCF\u6761\u683C\u5F0F\uFF1A
|
|
496
|
+
{"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "\u4E8B\u5B9E\u5185\u5BB9", "existingIdx": \u6574\u6570\u6216\u7701\u7565, "reason": "\u539F\u56E0\uFF08\u53EF\u9009\uFF09"}
|
|
497
|
+
|
|
498
|
+
\u6BCF\u6B21\u6700\u591A\u8FD4\u56DE 3 \u6761\u64CD\u4F5C\u3002\u5982\u679C\u6CA1\u6709\u503C\u5F97\u64CD\u4F5C\u7684\u5185\u5BB9\uFF0C\u8FD4\u56DE []\u3002
|
|
499
|
+
\u53EA\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
500
|
+
|
|
501
|
+
\u793A\u4F8B\uFF1A
|
|
502
|
+
|
|
503
|
+
1. \u63D0\u53D6\u65B0\u504F\u597D\uFF1A
|
|
504
|
+
[{"action": "NEW", "fact": "\u7528\u6237\u504F\u597D TypeScript \u800C\u975E JavaScript", "reason": "\u660E\u786E\u8868\u8FBE\u7684\u6280\u672F\u504F\u597D"}]
|
|
505
|
+
|
|
506
|
+
2. \u66F4\u65B0\u5DF2\u6709\u8BB0\u5FC6\uFF08idx 0 \u539F\u4E3A"\u7528\u6237\u6B63\u5728\u8BC4\u4F30 React \u548C Vue"\uFF09\uFF1A
|
|
507
|
+
[{"action": "UPDATE", "fact": "\u7528\u6237\u51B3\u5B9A\u4F7F\u7528 React\uFF08\u653E\u5F03\u4E86 Vue\uFF09", "existingIdx": 0, "reason": "\u51B3\u7B56\u5DF2\u660E\u786E\uFF0C\u66F4\u65B0\u8BC4\u4F30\u72B6\u6001"}]
|
|
508
|
+
|
|
509
|
+
3. \u5BF9\u8BDD\u4EC5\u4E3A"\u597D\u7684\uFF0C\u8C22\u8C22"/"\u6CA1\u95EE\u9898"\u7B49\u786E\u8BA4\u8BED\uFF0C\u65E0\u65B0\u4FE1\u606F\uFF1A
|
|
510
|
+
[]`,
|
|
511
|
+
shouldMerge: (contentA, contentB) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u53BB\u91CD\u52A9\u624B\uFF0C\u8D1F\u8D23\u5224\u65AD\u4E24\u6761\u8BB0\u5FC6\u662F\u5426\u8868\u8FBE\u4E86\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\u3002
|
|
512
|
+
|
|
513
|
+
\u8BB0\u5FC6A\uFF1A${contentA}
|
|
514
|
+
\u8BB0\u5FC6B\uFF1A${contentB}
|
|
515
|
+
|
|
516
|
+
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
517
|
+
- \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u8868\u8FBE\u7684\u662F\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\uFF08\u53EF\u80FD\u63AA\u8F9E\u4E0D\u540C\u3001\u7C92\u5EA6\u4E0D\u540C\uFF0C\u4F46\u6838\u5FC3\u4E8B\u5B9E\u4E00\u81F4\uFF09\uFF0C\u8FD4\u56DE JSON\uFF1A
|
|
518
|
+
{"shouldMerge": true, "merged": "\u5408\u5E76\u540E\u7684\u7B80\u6D01\u8868\u8FF0\uFF0C\u4FDD\u7559\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u952E\u4FE1\u606F\uFF0C\u6BD4\u4EFB\u4F55\u4E00\u6761\u90FD\u66F4\u5B8C\u6574"}
|
|
519
|
+
- \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u662F\u4E92\u8865\u4FE1\u606F\u3001\u4E0D\u540C\u4E3B\u9898\u3001\u6216\u5305\u542B\u4E0D\u540C\u7684\u5177\u4F53\u4E8B\u5B9E\uFF0C\u8FD4\u56DE JSON\uFF1A
|
|
520
|
+
{"shouldMerge": false}
|
|
521
|
+
|
|
522
|
+
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
|
|
523
|
+
|
|
524
|
+
\u793A\u4F8B\uFF1A
|
|
525
|
+
|
|
526
|
+
1. \u5E94\u5408\u5E76\uFF08\u7C92\u5EA6\u4E0D\u540C\uFF09\uFF1A
|
|
527
|
+
A: "\u9879\u76EE\u4F7F\u7528 PostgreSQL \u6570\u636E\u5E93"
|
|
528
|
+
B: "\u9879\u76EE\u7684\u4E3B\u6570\u636E\u5E93\u662F PostgreSQL 16\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"
|
|
529
|
+
\u2192 {"shouldMerge": true, "merged": "\u9879\u76EE\u4F7F\u7528 PostgreSQL 16 \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"}
|
|
530
|
+
|
|
531
|
+
2. \u4E0D\u5E94\u5408\u5E76\uFF08\u4E92\u8865\u4F46\u4E0D\u540C\uFF09\uFF1A
|
|
532
|
+
A: "\u7528\u6237\u559C\u6B22\u7528 VS Code"
|
|
533
|
+
B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
|
|
534
|
+
\u2192 {"shouldMerge": false}`,
|
|
535
|
+
evolutionJudge: (oldContent, newContent) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u6F14\u5316\u5224\u65AD\u52A9\u624B\u3002\u5206\u6790\u4EE5\u4E0B\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u7CFB\u5E76\u8FD4\u56DE JSON\u3002
|
|
536
|
+
|
|
537
|
+
\u65E7\u8BB0\u5FC6\uFF1A${oldContent}
|
|
538
|
+
\u65B0\u8BB0\u5FC6\uFF1A${newContent}
|
|
539
|
+
|
|
540
|
+
\u5224\u65AD\u89C4\u5219\uFF1A
|
|
541
|
+
|
|
542
|
+
- EVOLVE\uFF1A\u65B0\u5185\u5BB9\u662F\u5BF9\u65E7\u8BB0\u5FC6\u7684\u6DF1\u5316/\u66F4\u65B0\uFF08\u5982\u300C\u6B63\u5728\u8003\u8651\u7528 Next.js\u300D\u2192\u300C\u51B3\u5B9A\u7528 Next.js 14 App Router\u300D\uFF09
|
|
543
|
+
\u8FD4\u56DE\uFF1A{"type": "EVOLVE", "mergedContent": "\u878D\u5408\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u4FDD\u7559\u6F14\u5316\u8F68\u8FF9"}
|
|
544
|
+
|
|
545
|
+
- CONFLICT\uFF1A\u65B0\u65E7\u4FE1\u606F\u5728\u540C\u4E00\u5C5E\u6027\u4E0A\u76F4\u63A5\u77DB\u76FE\uFF08\u5982\u300C\u4F7F\u7528 MySQL \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
|
|
546
|
+
\u8FD4\u56DE\uFF1A{"type": "CONFLICT"}
|
|
547
|
+
|
|
548
|
+
- EXPAND\uFF1A\u65B0\u4FE1\u606F\u662F\u5BF9\u65E7\u8BB0\u5FC6\u540C\u4E00\u4E3B\u9898\u7684\u8865\u5145\u6269\u5C55\uFF08\u5982\u300C\u8D1F\u8D23\u540E\u7AEF\u5F00\u53D1\u300D+\u300C\u540E\u7AEF\u4F7F\u7528 Go \u548C gRPC\u300D\uFF09
|
|
549
|
+
\u8FD4\u56DE\uFF1A{"type": "EXPAND", "mergedContent": "\u5408\u5E76\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u6574\u5408\u53CC\u65B9\u4FE1\u606F"}
|
|
550
|
+
|
|
551
|
+
- NEW\uFF1A\u5168\u65B0\u4FE1\u606F\uFF0C\u4E0E\u65E7\u8BB0\u5FC6\u65E0\u5B9E\u8D28\u5173\u8054\uFF08\u5982\u300C\u559C\u6B22 dark mode\u300Dvs\u300C\u4E0B\u5468\u8981\u53BB\u51FA\u5DEE\u300D\uFF09
|
|
552
|
+
\u8FD4\u56DE\uFF1A{"type": "NEW"}
|
|
553
|
+
|
|
554
|
+
\u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`,
|
|
555
|
+
conflictScan: (numberedNotes) => `\u4F60\u5728\u5BA1\u8BA1\u4E00\u4E2A\u4EBA\u7684\u8BB0\u5FC6\u5E93\uFF0C\u627E\u51FA\u5176\u4E2D**\u4E92\u76F8\u77DB\u76FE**\u7684\u6761\u76EE\u3002
|
|
556
|
+
|
|
557
|
+
\u4E0B\u9762\u662F\u7F16\u53F7\u7684\u8BB0\u5FC6\u3002\u627E\u51FA\u90A3\u4E9B**\u4E0D\u53EF\u80FD\u540C\u65F6\u4E3A\u771F**\u7684\u914D\u5BF9\u3002
|
|
558
|
+
|
|
559
|
+
${numberedNotes}
|
|
560
|
+
|
|
561
|
+
\u7B97\u77DB\u76FE\u7684\u60C5\u51B5\uFF1A
|
|
562
|
+
- \u540C\u4E00\u5C5E\u6027\u4E0A\u51FA\u73B0\u4E92\u65A5\u7684\u503C\uFF08\u300C\u4F4F\u5728\u5DF4\u9ECE\u300Dvs\u300C\u642C\u5230\u4E86\u67CF\u6797\u300D\uFF09
|
|
563
|
+
- \u540E\u6765\u7684\u8BB0\u5FC6\u8FDD\u53CD\u4E86\u5148\u524D\u9648\u8FF0\u7684\u504F\u597D\u6216\u7EA6\u675F\uFF08\u300C\u5403\u7D20\u300Dvs\u300C\u90A3\u5757\u725B\u6392\u5F88\u597D\u5403\u300D\uFF09
|
|
564
|
+
- \u540E\u6765\u7684\u4E8B\u5B9E\u53D6\u4EE3\u4E86\u5148\u524D\u7684\uFF08\u300C\u7528 MySQL\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
|
|
565
|
+
|
|
566
|
+
**\u4E0D\u7B97**\u77DB\u76FE \u2014\u2014 \u8BF7\u4E25\u683C\uFF0C\u4EE5\u4E0B\u662F\u6700\u5E38\u89C1\u7684\u8BEF\u5224\uFF1A
|
|
567
|
+
- \u7D2F\u52A0\u7684\u4E8B\u5B9E\u3002\u4E24\u8005\u53EF\u4EE5\u540C\u65F6\u6210\u7ACB\uFF08\u300C\u517B\u4E86\u4E00\u53EA\u72D7\u53EB Buddy\u300D+\u300C\u53C8\u9886\u517B\u4E86\u7B2C\u4E8C\u53EA\u53EB Scout\u300D**\u4E0D\u662F**\u77DB\u76FE\uFF09
|
|
568
|
+
- \u4E24\u6761\u8BB0\u5FC6\u672C\u8EAB\u5DF2\u7ECF\u4F53\u73B0\u4E86\u968F\u65F6\u95F4\u7684\u53D8\u5316
|
|
569
|
+
- \u53EA\u662F\u4E3B\u9898\u76F8\u4F3C\u6216\u76F8\u5173
|
|
570
|
+
- \u573A\u666F\u4E0D\u540C\uFF08\u5728\u516C\u53F8\u559D\u5496\u5561\uFF0C\u5728\u5BB6\u559D\u8336\uFF09
|
|
571
|
+
|
|
572
|
+
\u5BF9\u6BCF\u4E00\u5BF9\u77DB\u76FE\uFF0C\u8FD8\u8981\u6307\u51FA\u54EA\u4E00\u6761\u662F**\u5DF2\u5931\u6548\u7684**\uFF08\u4E0D\u518D\u4E3A\u771F\u7684\u90A3\u6761\uFF09\u3002\u8BF7\u4ECE**\u63AA\u8F9E**\u5224\u65AD\uFF0C\u4E0D\u8981\u5047\u8BBE\u987A\u5E8F\uFF1A
|
|
573
|
+
\u300C\u4EE5\u524D\u300D\u300C2019 \u5E74\u90A3\u4F1A\u513F\u300D\u300C\u4E0A\u4E2A\u6708\u642C\u4E86\u300D\u300C\u6539\u7528\u4E86\u300D\u8FD9\u7C7B\u8BF4\u6CD5\u80FD\u544A\u8BC9\u4F60\u54EA\u6761\u63CF\u8FF0\u7684\u662F\u8FC7\u53BB\u3002
|
|
574
|
+
\u8FD9\u4E9B\u8BB0\u5FC6**\u4E0D\u662F\u6309\u65F6\u95F4\u987A\u5E8F\u6392\u5217\u7684**\uFF0C\u7F16\u53F7\u4E5F\u4E0D\u4EE3\u8868\u65B0\u65E7\u3002
|
|
575
|
+
|
|
576
|
+
\u5982\u679C\u65E0\u6CD5\u5224\u65AD\u54EA\u6761\u5DF2\u5931\u6548\uFF0C\u5C31\u586B null\u3002\u8FD9\u662F\u4E00\u4E2A**\u6B63\u5E38\u4E14\u6709\u7528**\u7684\u56DE\u7B54 \u2014\u2014 \u5B81\u53EF\u586B null \u4E5F\u4E0D\u8981\u731C\uFF0C
|
|
577
|
+
\u56E0\u4E3A\u731C\u9519\u4F1A\u8BA9\u4E00\u6761**\u4ECD\u7136\u4E3A\u771F**\u7684\u8BB0\u5FC6\u88AB\u505C\u7528\u3002
|
|
578
|
+
|
|
579
|
+
\u53EA\u8FD4\u56DE JSON \u6570\u7EC4\u3002\u6CA1\u6709\u771F\u6B63\u77DB\u76FE\u5C31\u8FD4\u56DE\u7A7A\u6570\u7EC4\uFF1A
|
|
580
|
+
[{"a": 0, "b": 3, "superseded": 0, "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u662F\u54EA\u4E2A\u5C5E\u6027\u4E92\u65A5"}]
|
|
581
|
+
|
|
582
|
+
"superseded" \u53EA\u80FD\u662F "a" \u7684\u503C\u3001"b" \u7684\u503C\uFF0C\u6216 null\u3002
|
|
583
|
+
\u4F7F\u7528\u4E0A\u9762\u663E\u793A\u7684\u7F16\u53F7\u3002\u540C\u4E00\u5BF9\u53EA\u62A5\u4E00\u6B21\u3002**\u5B81\u53EF\u4E0D\u62A5\uFF0C\u4E5F\u4E0D\u8981\u731C\u3002**`
|
|
584
|
+
};
|
|
585
|
+
var templates = { en, zh };
|
|
586
|
+
var t = templates[LOCALE];
|
|
587
|
+
|
|
588
|
+
// src/llm.ts
|
|
589
|
+
var _override = {};
|
|
590
|
+
var _warned = /* @__PURE__ */ new Set();
|
|
591
|
+
function warnOnce(key, message) {
|
|
592
|
+
if (_warned.has(key)) return;
|
|
593
|
+
_warned.add(key);
|
|
594
|
+
console.error(message);
|
|
595
|
+
}
|
|
596
|
+
function resolveProvider(role = "fast") {
|
|
597
|
+
const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
|
|
598
|
+
const p = (raw || process.env.AMEM_LLM_PROVIDER || _override.provider || "anthropic").trim().toLowerCase();
|
|
599
|
+
if (p !== "anthropic" && p !== "openai") {
|
|
600
|
+
warnOnce(`provider:${p}`, `[amem] unknown LLM provider "${p}"; falling back to anthropic`);
|
|
601
|
+
}
|
|
602
|
+
return p;
|
|
603
|
+
}
|
|
604
|
+
function resolveModel(role = "fast") {
|
|
605
|
+
const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_MODEL || _override.strong?.model || void 0 : void 0;
|
|
606
|
+
return strong || process.env.AMEM_LLM_MODEL || _override.model || (resolveProvider(role) === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
|
|
607
|
+
}
|
|
608
|
+
function resolveBaseURL(role = "fast") {
|
|
609
|
+
const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_BASE_URL || _override.strong?.baseURL || void 0 : void 0;
|
|
610
|
+
return strong || process.env.AMEM_LLM_BASE_URL || _override.baseURL || void 0;
|
|
611
|
+
}
|
|
612
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
613
|
+
function resolveTimeoutMs() {
|
|
614
|
+
const envVal = Number(process.env.AMEM_LLM_TIMEOUT);
|
|
615
|
+
if (Number.isFinite(envVal) && envVal > 0) return envVal;
|
|
616
|
+
if (_override.timeoutMs && _override.timeoutMs > 0) return _override.timeoutMs;
|
|
617
|
+
return DEFAULT_TIMEOUT_MS;
|
|
618
|
+
}
|
|
619
|
+
var _anthropicClients = /* @__PURE__ */ new Map();
|
|
620
|
+
function anthropic(baseURL) {
|
|
621
|
+
const key = baseURL ?? "";
|
|
622
|
+
let client = _anthropicClients.get(key);
|
|
623
|
+
if (!client) {
|
|
624
|
+
client = new import_sdk.default({
|
|
625
|
+
...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
|
|
626
|
+
...baseURL && { baseURL },
|
|
627
|
+
timeout: resolveTimeoutMs()
|
|
628
|
+
});
|
|
629
|
+
_anthropicClients.set(key, client);
|
|
630
|
+
}
|
|
631
|
+
return client;
|
|
632
|
+
}
|
|
633
|
+
var _openaiClients = /* @__PURE__ */ new Map();
|
|
634
|
+
function openai(baseURL) {
|
|
635
|
+
const key = baseURL ?? "";
|
|
636
|
+
let client = _openaiClients.get(key);
|
|
637
|
+
if (!client) {
|
|
638
|
+
client = new import_openai.default({
|
|
639
|
+
// AMEM_LLM_API_KEY first (engine convention), then the SDK's own
|
|
640
|
+
// OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
|
|
641
|
+
// env fallback, so read it here. Placeholder last, so keyless local servers
|
|
642
|
+
// (Ollama, vLLM) still work.
|
|
643
|
+
apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
|
|
644
|
+
...baseURL && { baseURL },
|
|
645
|
+
timeout: resolveTimeoutMs()
|
|
646
|
+
});
|
|
647
|
+
_openaiClients.set(key, client);
|
|
648
|
+
}
|
|
649
|
+
return client;
|
|
650
|
+
}
|
|
651
|
+
async function llmCall(prompt, maxTokens = 500, role = "fast") {
|
|
652
|
+
const provider = resolveProvider(role);
|
|
653
|
+
const model = resolveModel(role);
|
|
654
|
+
const baseURL = resolveBaseURL(role);
|
|
655
|
+
const isThinking = model.includes("gemini") || model.includes("pro-agent");
|
|
656
|
+
const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
|
|
657
|
+
try {
|
|
658
|
+
return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
|
|
659
|
+
} catch (e) {
|
|
660
|
+
console.error(`[amem] LLM call failed: ${e.message}`);
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function anthropicCall(prompt, model, maxTokens, baseURL) {
|
|
665
|
+
const resp = await anthropic(baseURL).messages.create({
|
|
666
|
+
model,
|
|
667
|
+
max_tokens: maxTokens,
|
|
668
|
+
messages: [{ role: "user", content: prompt }]
|
|
669
|
+
});
|
|
670
|
+
for (const block of resp.content) {
|
|
671
|
+
if (block.type === "text") return block.text.trim();
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
async function openaiCall(prompt, model, maxTokens, baseURL) {
|
|
676
|
+
const isReasoning = /^o\d/.test(model) || model.startsWith("gpt-5");
|
|
677
|
+
const resp = await openai(baseURL).chat.completions.create({
|
|
678
|
+
model,
|
|
679
|
+
...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
|
|
680
|
+
messages: [{ role: "user", content: prompt }]
|
|
681
|
+
});
|
|
682
|
+
return resp.choices[0]?.message?.content?.trim() ?? null;
|
|
683
|
+
}
|
|
684
|
+
function stripReasoning(raw) {
|
|
685
|
+
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<\|(?:eot_id|im_start|im_end|begin_of_text|end_of_text|endoftext)\|>/g, "").trim();
|
|
686
|
+
}
|
|
687
|
+
function stripFences(raw) {
|
|
688
|
+
raw = stripReasoning(raw);
|
|
689
|
+
if (raw.startsWith("```")) {
|
|
690
|
+
const lines = raw.split("\n");
|
|
691
|
+
lines.shift();
|
|
692
|
+
if (lines[lines.length - 1] === "```") lines.pop();
|
|
693
|
+
raw = lines.join("\n").trim();
|
|
694
|
+
}
|
|
695
|
+
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
696
|
+
try {
|
|
697
|
+
raw = JSON.parse(raw);
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return raw;
|
|
702
|
+
}
|
|
703
|
+
function parseJsonLoose(raw) {
|
|
704
|
+
const cleaned = stripFences(raw);
|
|
705
|
+
try {
|
|
706
|
+
return JSON.parse(cleaned);
|
|
707
|
+
} catch (e) {
|
|
708
|
+
const m = cleaned.match(/\{[\s\S]*\}/);
|
|
709
|
+
if (m) return JSON.parse(m[0]);
|
|
710
|
+
throw e;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
var VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
714
|
+
var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
715
|
+
"Technical",
|
|
716
|
+
"Business",
|
|
717
|
+
"Personal",
|
|
718
|
+
"Project",
|
|
719
|
+
"Research",
|
|
720
|
+
"System",
|
|
721
|
+
"General"
|
|
722
|
+
]);
|
|
723
|
+
async function llmConstructNote(content) {
|
|
724
|
+
const prompt = `Analyze the following text and respond with valid JSON only (no markdown fences, no explanation, no comments). All string values must use standard double quotes and be properly escaped:
|
|
725
|
+
{
|
|
726
|
+
"keywords": ["keyword1", "keyword2"],
|
|
727
|
+
"tags": ["tag1", "tag2"],
|
|
728
|
+
"context": "one sentence summary in the same language as the input",
|
|
729
|
+
"category": "Technical|Business|Personal|Project|Research|System|General",
|
|
730
|
+
"note_type": "memory|knowledge",
|
|
731
|
+
"topics": ["Topic1", "Topic2"],
|
|
732
|
+
"confidence": "high|medium|low"
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
Category guide:
|
|
736
|
+
- Technical: code, tools, configuration, APIs, debugging
|
|
737
|
+
- Business: company, finance, compliance, contracts, invoices
|
|
738
|
+
- Personal: personal state, habits, preferences, emotions
|
|
739
|
+
- Project: project progress, decisions, milestones
|
|
740
|
+
- Research: research, literature, evaluation, comparison
|
|
741
|
+
- System: system services, monitoring, operations
|
|
742
|
+
- General: anything that does not fit the above
|
|
743
|
+
|
|
744
|
+
note_type guide:
|
|
745
|
+
- knowledge: books, methodologies, tools, domain knowledge, reference material \u2014 durable, no strong time component
|
|
746
|
+
- memory: events, decisions, preferences, states, observations \u2014 episodic, time-sensitive
|
|
747
|
+
|
|
748
|
+
topics guide (Story 26B):
|
|
749
|
+
- Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].
|
|
750
|
+
- List 1-5 concise subject tags representing the main topics of this knowledge, e.g. ["TypeScript", "Qdrant", "Vector DB"].
|
|
751
|
+
|
|
752
|
+
confidence guide (Story 27):
|
|
753
|
+
- high: note_type is unambiguous \u2014 clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)
|
|
754
|
+
- medium: some ambiguity \u2014 e.g. "learned X method" could be either memory or knowledge
|
|
755
|
+
- low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
|
|
756
|
+
|
|
757
|
+
Text: ${content}`;
|
|
758
|
+
const raw = await llmCall(prompt, 400);
|
|
759
|
+
if (!raw)
|
|
760
|
+
return {
|
|
761
|
+
keywords: [],
|
|
762
|
+
tags: [],
|
|
763
|
+
context: "",
|
|
764
|
+
category: "General",
|
|
765
|
+
note_type: "memory",
|
|
766
|
+
topics: [],
|
|
767
|
+
confidence: "medium"
|
|
768
|
+
};
|
|
769
|
+
try {
|
|
770
|
+
const data = parseJsonLoose(raw);
|
|
771
|
+
const rawCategory = typeof data.category === "string" ? data.category : "General";
|
|
772
|
+
const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
|
|
773
|
+
const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
|
|
774
|
+
const topics = note_type === "knowledge" && Array.isArray(data.topics) ? data.topics.filter((v) => typeof v === "string") : [];
|
|
775
|
+
const rawConfidence = typeof data.confidence === "string" ? data.confidence : "medium";
|
|
776
|
+
const confidence = VALID_CONFIDENCE.has(rawConfidence) ? rawConfidence : "medium";
|
|
777
|
+
return {
|
|
778
|
+
keywords: Array.isArray(data.keywords) ? data.keywords : [],
|
|
779
|
+
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
780
|
+
context: typeof data.context === "string" ? data.context : "",
|
|
781
|
+
category,
|
|
782
|
+
note_type,
|
|
783
|
+
topics,
|
|
784
|
+
confidence
|
|
785
|
+
};
|
|
786
|
+
} catch (e) {
|
|
787
|
+
console.error(`[amem] Note construction parse failed: ${e.message}`);
|
|
788
|
+
return {
|
|
789
|
+
keywords: [],
|
|
790
|
+
tags: [],
|
|
791
|
+
context: "",
|
|
792
|
+
category: "General",
|
|
793
|
+
note_type: "memory",
|
|
794
|
+
topics: [],
|
|
795
|
+
confidence: "medium"
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/memory.ts
|
|
801
|
+
var import_uuid = require("uuid");
|
|
802
|
+
|
|
803
|
+
// src/config.ts
|
|
804
|
+
var os = __toESM(require("os"), 1);
|
|
805
|
+
var path = __toESM(require("path"), 1);
|
|
806
|
+
var _dataDir = process.env.AMEM_DATA_DIR || path.join(os.homedir(), ".amem");
|
|
807
|
+
|
|
808
|
+
// src/memory.ts
|
|
809
|
+
var import_jieba = require("@node-rs/jieba");
|
|
810
|
+
function buildEmbedText(note) {
|
|
811
|
+
let text = note.content;
|
|
812
|
+
if (note.keywords.length) text += " " + note.keywords.join(" ");
|
|
813
|
+
if (note.tags.length) text += " " + note.tags.join(" ");
|
|
814
|
+
if (note.context) text += " " + note.context;
|
|
815
|
+
return text;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/migrate.ts
|
|
819
|
+
function missingDerivedFields(n) {
|
|
820
|
+
return n.keywords.length === 0 || n.tags.length === 0;
|
|
821
|
+
}
|
|
822
|
+
async function migrateCollection(opts) {
|
|
823
|
+
const { from, to } = opts;
|
|
824
|
+
const refreshFields = opts.refreshFields !== false;
|
|
825
|
+
const dryRun = opts.dryRun !== false;
|
|
826
|
+
const log = opts.logger?.info ?? ((m) => console.log(m));
|
|
827
|
+
const warn = opts.logger?.warn ?? ((m) => console.warn(m));
|
|
828
|
+
if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
|
|
829
|
+
const model = getEmbeddingModel();
|
|
830
|
+
const targetDim = await getEmbeddingDim();
|
|
831
|
+
const sourceDim = await collectionDimRaw(from);
|
|
832
|
+
if (sourceDim === null) throw new Error(`migrate: source collection "${from}" does not exist`);
|
|
833
|
+
const points = await scrollAllRaw(from);
|
|
834
|
+
const notes = points.map(pointToNote);
|
|
835
|
+
const missingDerived = notes.filter(missingDerivedFields).length;
|
|
836
|
+
log(
|
|
837
|
+
`[migrate] ${from} (${sourceDim}d, ${notes.length} notes) \u2192 ${to} (${targetDim}d, ${model}); ${missingDerived} note(s) missing keywords/tags`
|
|
838
|
+
);
|
|
839
|
+
if (dryRun) {
|
|
840
|
+
log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
|
|
841
|
+
return {
|
|
842
|
+
total: notes.length,
|
|
843
|
+
missingDerived,
|
|
844
|
+
refreshed: 0,
|
|
845
|
+
migrated: 0,
|
|
846
|
+
skipped: 0,
|
|
847
|
+
sourceDim,
|
|
848
|
+
targetDim,
|
|
849
|
+
model,
|
|
850
|
+
dryRun: true
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
let alreadyDone = /* @__PURE__ */ new Set();
|
|
854
|
+
const existingTargetDim = await collectionDimRaw(to);
|
|
855
|
+
if (existingTargetDim === null) {
|
|
856
|
+
await createCollectionRaw(to, targetDim);
|
|
857
|
+
log(`[migrate] created ${to} at ${targetDim}d`);
|
|
858
|
+
} else {
|
|
859
|
+
if (existingTargetDim !== targetDim) {
|
|
860
|
+
throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
|
|
861
|
+
}
|
|
862
|
+
const present = await scrollIdsRaw(to);
|
|
863
|
+
if (present.size > 0) {
|
|
864
|
+
const sourceIds = new Set(notes.map((n) => n.id));
|
|
865
|
+
const foreign = [...present].filter((id) => !sourceIds.has(id));
|
|
866
|
+
if (foreign.length > 0) {
|
|
867
|
+
throw new Error(
|
|
868
|
+
`migrate: target "${to}" holds ${foreign.length} point(s) that are not in "${from}" (e.g. ${foreign[0]}). That is not an interrupted migration \u2014 use a different target.`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
alreadyDone = present;
|
|
872
|
+
log(`[migrate] resuming: ${present.size} of ${notes.length} already in ${to}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
let refreshed = 0;
|
|
876
|
+
let migrated = 0;
|
|
877
|
+
const BATCH = 64;
|
|
878
|
+
let buffer = [];
|
|
879
|
+
const flush = async () => {
|
|
880
|
+
if (!buffer.length) return;
|
|
881
|
+
await upsertPointsRaw(to, buffer);
|
|
882
|
+
migrated += buffer.length;
|
|
883
|
+
buffer = [];
|
|
884
|
+
};
|
|
885
|
+
for (const note of notes) {
|
|
886
|
+
if (alreadyDone.has(note.id)) continue;
|
|
887
|
+
if (refreshFields && missingDerivedFields(note)) {
|
|
888
|
+
try {
|
|
889
|
+
const built = await llmConstructNote(note.content);
|
|
890
|
+
if (note.keywords.length === 0) note.keywords = built.keywords;
|
|
891
|
+
if (note.tags.length === 0) note.tags = built.tags;
|
|
892
|
+
if (!note.context) note.context = built.context;
|
|
893
|
+
refreshed++;
|
|
894
|
+
} catch (e) {
|
|
895
|
+
warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
|
|
899
|
+
buffer.push(point);
|
|
900
|
+
if (buffer.length >= BATCH) {
|
|
901
|
+
await flush();
|
|
902
|
+
log(`[migrate] ${alreadyDone.size + migrated}/${notes.length}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
await flush();
|
|
906
|
+
const finalCount = await countPointsRaw(to);
|
|
907
|
+
if (finalCount !== notes.length) {
|
|
908
|
+
warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
|
|
909
|
+
}
|
|
910
|
+
log(`[migrate] done: ${migrated} written, ${alreadyDone.size} already present, ${refreshed} re-extracted.`);
|
|
911
|
+
return {
|
|
912
|
+
total: notes.length,
|
|
913
|
+
missingDerived,
|
|
914
|
+
refreshed,
|
|
915
|
+
migrated,
|
|
916
|
+
skipped: alreadyDone.size,
|
|
917
|
+
sourceDim,
|
|
918
|
+
targetDim,
|
|
919
|
+
model,
|
|
920
|
+
dryRun: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
async function switchToMigrated(opts) {
|
|
924
|
+
const { name, to } = opts;
|
|
925
|
+
const log = opts.logger?.info ?? ((m) => console.log(m));
|
|
926
|
+
if (name === to) throw new Error(`switch: "${name}" and "${to}" are the same collection`);
|
|
927
|
+
const already = await resolveAliasRaw(name);
|
|
928
|
+
if (already === to) {
|
|
929
|
+
log(`[switch] "${name}" already points at "${to}" \u2014 nothing to do`);
|
|
930
|
+
return { name, to, moved: await countPointsRaw(to) };
|
|
931
|
+
}
|
|
932
|
+
const targetCount = await countPointsRaw(to);
|
|
933
|
+
if (targetCount === 0) throw new Error(`switch: "${to}" is empty \u2014 migrate into it first`);
|
|
934
|
+
if (already === null) {
|
|
935
|
+
const sourceCount = await countPointsRaw(name);
|
|
936
|
+
if (targetCount < sourceCount) {
|
|
937
|
+
throw new Error(
|
|
938
|
+
`switch: "${to}" holds ${targetCount} point(s) but "${name}" still holds ${sourceCount}. The migration is not finished \u2014 run it again before switching.`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
log(`[switch] verified ${targetCount} in "${to}" against ${sourceCount} in "${name}"`);
|
|
942
|
+
await deleteCollectionRaw(name);
|
|
943
|
+
log(`[switch] dropped "${name}"`);
|
|
944
|
+
await createAliasRaw(name, to);
|
|
945
|
+
} else {
|
|
946
|
+
await setAliasRaw(name, to);
|
|
947
|
+
}
|
|
948
|
+
log(`[switch] "${name}" now resolves to "${to}"`);
|
|
949
|
+
return { name, to, moved: targetCount };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/cli-migrate.ts
|
|
953
|
+
var USAGE = `amem-migrate \u2014 move a memory store onto a different embedding model
|
|
954
|
+
|
|
955
|
+
amem-migrate what state the store is in, and what comes next
|
|
956
|
+
amem-migrate --apply do the next step; safe to interrupt and re-run
|
|
957
|
+
amem-migrate --switch put the new store behind the old name (irreversible)
|
|
958
|
+
|
|
959
|
+
Options
|
|
960
|
+
--from-collection <name> the store to migrate. Defaults to AMEM_COLLECTION.
|
|
961
|
+
--to-collection <name> where to build it. Derived from the source if omitted.
|
|
962
|
+
--no-refresh-fields skip re-extracting keywords for notes that never had
|
|
963
|
+
them. Makes the run completely offline.
|
|
964
|
+
-h, --help
|
|
965
|
+
|
|
966
|
+
Migrates onto amem's current default unless AMEM_EMBED_MODEL says otherwise:
|
|
967
|
+
|
|
968
|
+
AMEM_EMBED_MODEL=Alibaba-NLP/gte-multilingual-base amem-migrate --apply
|
|
969
|
+
|
|
970
|
+
Nothing before --switch touches the original. If a run looks wrong, delete the
|
|
971
|
+
target and start again.`;
|
|
972
|
+
function parseArgs(argv) {
|
|
973
|
+
const value = (flag) => {
|
|
974
|
+
const i = argv.indexOf(flag);
|
|
975
|
+
return i === -1 ? void 0 : argv[i + 1];
|
|
976
|
+
};
|
|
977
|
+
return {
|
|
978
|
+
help: argv.includes("-h") || argv.includes("--help"),
|
|
979
|
+
apply: argv.includes("--apply"),
|
|
980
|
+
switchOver: argv.includes("--switch"),
|
|
981
|
+
from: value("--from-collection"),
|
|
982
|
+
to: value("--to-collection"),
|
|
983
|
+
refreshFields: !argv.includes("--no-refresh-fields")
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function deriveTarget(source) {
|
|
987
|
+
const m = source.match(/^(.*)_v(\d+)$/);
|
|
988
|
+
return m ? `${m[1]}_v${Number(m[2]) + 1}` : `${source}_v2`;
|
|
989
|
+
}
|
|
990
|
+
function carried(args) {
|
|
991
|
+
return (args.from ? ` --from-collection ${args.from}` : "") + (args.to ? ` --to-collection ${args.to}` : "");
|
|
992
|
+
}
|
|
993
|
+
async function detect(from, to) {
|
|
994
|
+
const alias = await resolveAliasRaw(from);
|
|
995
|
+
if (alias !== null) return { kind: "switched", points: await countPointsRaw(from) };
|
|
996
|
+
const sourceDim = await collectionDimRaw(from);
|
|
997
|
+
if (sourceDim === null) return { kind: "no-source" };
|
|
998
|
+
const modelDim = await getEmbeddingDim();
|
|
999
|
+
if (sourceDim === modelDim) return { kind: "already-current", model: getEmbeddingModel() };
|
|
1000
|
+
const notes = await countPointsRaw(from);
|
|
1001
|
+
const targetDim = await collectionDimRaw(to);
|
|
1002
|
+
if (targetDim === null) return { kind: "not-started", notes };
|
|
1003
|
+
const done = (await scrollIdsRaw(to)).size;
|
|
1004
|
+
return done >= notes ? { kind: "ready-to-switch", notes } : { kind: "partial", done, notes };
|
|
1005
|
+
}
|
|
1006
|
+
async function main() {
|
|
1007
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1008
|
+
if (args.help) {
|
|
1009
|
+
console.log(USAGE);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const from = args.from ?? getCollection();
|
|
1013
|
+
const to = args.to ?? deriveTarget(from);
|
|
1014
|
+
const flags = carried(args);
|
|
1015
|
+
const phase = await detect(from, to);
|
|
1016
|
+
console.log(`store: ${from}`);
|
|
1017
|
+
console.log(`model: ${getEmbeddingModel()}`);
|
|
1018
|
+
switch (phase.kind) {
|
|
1019
|
+
case "no-source":
|
|
1020
|
+
console.error(`
|
|
1021
|
+
No collection named "${from}". Nothing to migrate.`);
|
|
1022
|
+
process.exitCode = 1;
|
|
1023
|
+
return;
|
|
1024
|
+
case "switched":
|
|
1025
|
+
console.log(`
|
|
1026
|
+
"${from}" is already an alias \u2014 ${phase.points} notes, nothing to do.`);
|
|
1027
|
+
return;
|
|
1028
|
+
case "already-current":
|
|
1029
|
+
console.log(`
|
|
1030
|
+
Already on ${phase.model}. Nothing to migrate.`);
|
|
1031
|
+
return;
|
|
1032
|
+
case "not-started":
|
|
1033
|
+
if (!args.apply) {
|
|
1034
|
+
console.log(`
|
|
1035
|
+
${phase.notes} notes to rebuild into "${to}".`);
|
|
1036
|
+
console.log(`Run "amem-migrate${flags} --apply" to start. "${from}" is only read.`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
break;
|
|
1040
|
+
case "partial":
|
|
1041
|
+
if (!args.apply) {
|
|
1042
|
+
console.log(`
|
|
1043
|
+
${phase.done} of ${phase.notes} rebuilt into "${to}".`);
|
|
1044
|
+
console.log(`Run "amem-migrate${flags} --apply" to carry on from there.`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
break;
|
|
1048
|
+
case "ready-to-switch":
|
|
1049
|
+
if (!args.switchOver) {
|
|
1050
|
+
console.log(`
|
|
1051
|
+
All ${phase.notes} notes are in "${to}". "${from}" is untouched.`);
|
|
1052
|
+
console.log(`Check it, then run "amem-migrate${flags} --switch" to put "${to}" behind the name "${from}".`);
|
|
1053
|
+
console.log(`That drops "${from}" and cannot be undone.`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
await switchToMigrated({ name: from, to });
|
|
1057
|
+
console.log(`
|
|
1058
|
+
Done. Nothing to change in your config \u2014 "${from}" now resolves to "${to}".`);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (args.switchOver) {
|
|
1062
|
+
console.error(`
|
|
1063
|
+
Not finished yet \u2014 run "amem-migrate${flags} --apply" until it is before switching.`);
|
|
1064
|
+
process.exitCode = 1;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const result = await migrateCollection({ from, to, dryRun: false, refreshFields: args.refreshFields });
|
|
1068
|
+
console.log(`
|
|
1069
|
+
${result.migrated + result.skipped} of ${result.total} rebuilt.`);
|
|
1070
|
+
if (result.migrated + result.skipped >= result.total) {
|
|
1071
|
+
console.log(`Check "${to}", then run "amem-migrate${flags} --switch".`);
|
|
1072
|
+
} else {
|
|
1073
|
+
console.log(`Run "amem-migrate${flags} --apply" again to carry on.`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
main().catch((err) => {
|
|
1077
|
+
console.error(`amem-migrate: ${err instanceof Error ? err.message : String(err)}`);
|
|
1078
|
+
process.exitCode = 1;
|
|
1079
|
+
});
|
|
1080
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1081
|
+
0 && (module.exports = {
|
|
1082
|
+
carried,
|
|
1083
|
+
deriveTarget,
|
|
1084
|
+
parseArgs
|
|
1085
|
+
});
|
|
1086
|
+
//# sourceMappingURL=cli-migrate.cjs.map
|