openclaw-memory-alibaba-local 1.0.5 → 1.0.6

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.
Files changed (3) hide show
  1. package/index.ts +198 -31
  2. package/package.json +1 -1
  3. package/prompts.ts +57 -0
package/index.ts CHANGED
@@ -63,6 +63,7 @@ import type { MemoryEntry, MemorySearchResult } from "./db.js";
63
63
  import {
64
64
  buildMemoryExtractionPrompt,
65
65
  buildUserImageExtractionPrompt,
66
+ buildWorldImageExtractionPrompt,
66
67
  SELF_IMPROVING_EXTRACTION_INSTRUCTIONS,
67
68
  } from "./prompts.js";
68
69
  import { extractUserQueryForRecall, stripForLogicalMemoryExtraction } from "./prompt-strip.js";
@@ -782,6 +783,119 @@ async function extractUserImageWithLLM(
782
783
  }
783
784
  }
784
785
 
786
+ // ---------------------------------------------------------------------------
787
+ // World image extraction: recall + LLM CRUD for world facts
788
+ // ---------------------------------------------------------------------------
789
+
790
+ type WorldImageAction =
791
+ | { action: "insert"; text: string; importance: number }
792
+ | { action: "update"; memoryId: string; text: string; importance: number }
793
+ | { action: "delete"; memoryId: string }
794
+ | { action: "skip" };
795
+
796
+ /**
797
+ * World image extraction: given N new world-fact extractions + M existing
798
+ * similar world-fact memories, ask LLM to decide insert / update / skip / delete for each.
799
+ */
800
+ async function extractWorldImageWithLLM(
801
+ llmConfig: LLMConfig,
802
+ newItems: LLMExtractionItem[],
803
+ existingCandidates: MemorySearchResult[],
804
+ ): Promise<WorldImageAction[]> {
805
+ if (newItems.length === 0) return [];
806
+
807
+ const newForPrompt = newItems.map((item, i) => ({
808
+ index: i,
809
+ text: item.text,
810
+ importance: item.importance,
811
+ }));
812
+ const existingForPrompt = existingCandidates.map((r) => ({
813
+ id: r.entry.id,
814
+ text: r.entry.text,
815
+ }));
816
+
817
+ const prompt = buildWorldImageExtractionPrompt(newForPrompt, existingForPrompt);
818
+ logLlmCall("worldImageExtraction", prompt.length);
819
+
820
+ const openai = new OpenAI({
821
+ apiKey: llmConfig.apiKey,
822
+ baseURL: llmConfig.baseUrl,
823
+ });
824
+
825
+ const completion = await openai.chat.completions.create({
826
+ model: llmConfig.model,
827
+ messages: [{ role: "user", content: prompt }],
828
+ temperature: 0,
829
+ max_tokens: 8192,
830
+ });
831
+ const raw = completion.choices[0]?.message?.content?.trim() ?? "";
832
+
833
+ const existingIdSet = new Set(existingCandidates.map((r) => r.entry.id));
834
+
835
+ try {
836
+ const parsed = JSON.parse(stripMarkdownJsonFence(raw)) as {
837
+ actions?: Array<{
838
+ index?: number;
839
+ action?: string;
840
+ text?: string;
841
+ importance?: unknown;
842
+ memoryId?: string;
843
+ }>;
844
+ };
845
+ const list = Array.isArray(parsed.actions) ? parsed.actions : [];
846
+ const resultMap = new Map<number, WorldImageAction[]>();
847
+ // Default: insert for each index
848
+ for (let i = 0; i < newItems.length; i++) {
849
+ resultMap.set(i, [{
850
+ action: "insert" as const,
851
+ text: newItems[i]!.text,
852
+ importance: newItems[i]!.importance,
853
+ }]);
854
+ }
855
+
856
+ for (const a of list) {
857
+ const idx = typeof a.index === "number" ? a.index : -1;
858
+ if (idx < 0 || idx >= newItems.length) continue;
859
+
860
+ if (a.action === "skip") {
861
+ resultMap.set(idx, [{ action: "skip" }]);
862
+ } else if (a.action === "delete" && typeof a.memoryId === "string" && existingIdSet.has(a.memoryId)) {
863
+ const existing = resultMap.get(idx) ?? [];
864
+ resultMap.set(idx, [{ action: "delete", memoryId: a.memoryId }, ...existing.filter(x => x.action !== "skip")]);
865
+ } else if (a.action === "update" && typeof a.memoryId === "string" && existingIdSet.has(a.memoryId)) {
866
+ const text = typeof a.text === "string" ? a.text.trim() : "";
867
+ if (text.length < 10) continue;
868
+ resultMap.set(idx, [{
869
+ action: "update",
870
+ memoryId: a.memoryId,
871
+ text,
872
+ importance: clampImportance(a.importance ?? newItems[idx]!.importance),
873
+ }]);
874
+ } else if (a.action === "insert") {
875
+ const text = typeof a.text === "string" ? a.text.trim() : "";
876
+ const finalText = text.length >= 10 ? text : newItems[idx]!.text;
877
+ resultMap.set(idx, [{
878
+ action: "insert",
879
+ text: finalText,
880
+ importance: clampImportance(a.importance ?? newItems[idx]!.importance),
881
+ }]);
882
+ }
883
+ }
884
+ const result: WorldImageAction[] = [];
885
+ for (let i = 0; i < newItems.length; i++) {
886
+ result.push(...(resultMap.get(i) ?? []));
887
+ }
888
+ return result;
889
+ } catch (err: unknown) {
890
+ console.warn(`[openclaw-memory-alibaba-local] worldImageExtraction JSON parse failed, fallback insert all: ${err}`);
891
+ return newItems.map((item) => ({
892
+ action: "insert" as const,
893
+ text: item.text,
894
+ importance: item.importance,
895
+ }));
896
+ }
897
+ }
898
+
785
899
  function shouldCapture(text: string, options?: { maxChars?: number }): boolean {
786
900
  const maxChars = options?.maxChars ?? DEFAULT_CAPTURE_MAX_CHARS;
787
901
  if (text.length < 10 || text.length > maxChars) return false;
@@ -1158,50 +1272,103 @@ async function captureUserMemoryFromInboundTexts(
1158
1272
  // ---- Parallel: event-item pipeline & user-item pipeline ----
1159
1273
  const eventPipeline = async () => {
1160
1274
  if (eventItems.length === 0) return;
1161
- if (cfg.memory_duplication_conflict_process) {
1162
- type EmbeddedItem = { item: LLMExtractionItem; vectors: number[][]; primary: number[] };
1163
- const embedded: EmbeddedItem[] = [];
1164
- for (const item of eventItems) {
1165
- const truncated = truncateForCapture(item.text, cfg.captureMaxChars);
1166
- const { vectors } = await backend.encodeForStorage(truncated);
1167
- embedded.push({ item: { ...item, text: truncated }, vectors, primary: vectors[0] });
1275
+
1276
+ // --- No LLM fallback: simple dedup ---
1277
+ if (!cfg.llm) {
1278
+ for (const e of eventItems) {
1279
+ const text = truncateForCapture(e.text, cfg.captureMaxChars);
1280
+ if (await db.existsSemanticDuplicate(agentId, sessionKey, e.category, text)) {
1281
+ continue;
1282
+ }
1283
+ await storeOneCaptureItem(agentId, { category: e.category, text, importance: e.importance }, cfg, db, backend, {
1284
+ userId,
1285
+ sessionId: sessionKey,
1286
+ });
1168
1287
  }
1169
-
1170
- const clusters = greedyCluster(embedded, (e) => e.primary, (e) => extractDatePrefix(e.item.text), 0.85, 3);
1171
-
1172
- for (const cluster of clusters) {
1173
- const text = concatDedupeDate(cluster.map((e) => e.item.text));
1174
- const importance = Math.max(...cluster.map((e) => e.item.importance));
1175
- const { vectors } = await backend.encodeForStorage(text);
1288
+ return;
1289
+ }
1290
+
1291
+ // --- LLM path: recall + CRUD ---
1292
+
1293
+ // 1. Batch embed all event items
1294
+ const embeddingResults: { item: LLMExtractionItem; vectors: number[][] }[] = [];
1295
+ for (const item of eventItems) {
1296
+ const truncated = truncateForCapture(item.text, cfg.captureMaxChars);
1297
+ const { vectors } = await backend.encodeForStorage(truncated);
1298
+ embeddingResults.push({ item: { ...item, text: truncated }, vectors });
1299
+ }
1300
+
1301
+ // 2. Recall top-3 similar existing world_facts per item, merge & dedup (cap ~10)
1302
+ const allVectors = embeddingResults.flatMap((r) => r.vectors);
1303
+ const recallMinScore = Math.max(0.5, cfg.similarityThresholdUserMemory - 0.35);
1304
+ const existingCandidates = allVectors.length > 0
1305
+ ? await db.searchMerged(agentId, allVectors, 3, recallMinScore, [WORLD_FACT])
1306
+ : [];
1307
+
1308
+ // 3. LLM CRUD decision
1309
+ console.log(`[openclaw-memory-alibaba-local] worldImageExtraction input: ${eventItems.length} event items, ${existingCandidates.length} existing candidates`);
1310
+ const worldActions = await extractWorldImageWithLLM(
1311
+ cfg.llm,
1312
+ embeddingResults.map((r) => r.item),
1313
+ existingCandidates,
1314
+ ).catch((err: unknown) => {
1315
+ console.warn(`[openclaw-memory-alibaba-local] worldImageExtraction LLM failed, fallback insert all: ${err}`);
1316
+ return embeddingResults.map((r): WorldImageAction => ({
1317
+ action: "insert" as const,
1318
+ text: r.item.text,
1319
+ importance: r.item.importance,
1320
+ }));
1321
+ });
1322
+
1323
+ // 4. Execute actions
1324
+ let insertCount = 0;
1325
+ for (const action of worldActions) {
1326
+ if (action.action === "skip") continue;
1327
+
1328
+ if (action.action === "delete") {
1329
+ const hit = existingCandidates.find((c) => c.entry.id === action.memoryId);
1330
+ if (hit) {
1331
+ await deleteSimilarLogicalMemory(db, agentId, sessionKey, hit);
1332
+ }
1333
+ continue;
1334
+ }
1335
+
1336
+ if (action.action === "update") {
1337
+ const hit = existingCandidates.find((c) => c.entry.id === action.memoryId);
1338
+ if (hit) {
1339
+ await deleteSimilarLogicalMemory(db, agentId, sessionKey, hit);
1340
+ }
1341
+ const { vectors } = await backend.encodeForStorage(action.text);
1176
1342
  const rows = buildChunkRows(
1177
- { category: WORLD_FACT as MemoryCategory, text, importance },
1343
+ { category: WORLD_FACT as MemoryCategory, text: action.text, importance: action.importance },
1178
1344
  vectors,
1179
1345
  { userId, sessionId: sessionKey },
1180
1346
  );
1181
1347
  await db.storeMany(agentId, rows);
1348
+ insertCount++;
1349
+ } else {
1350
+ // insert
1351
+ const { vectors } = await backend.encodeForStorage(action.text);
1352
+ const rows = buildChunkRows(
1353
+ { category: WORLD_FACT as MemoryCategory, text: action.text, importance: action.importance },
1354
+ vectors,
1355
+ { userId, sessionId: sessionKey },
1356
+ );
1357
+ await db.storeMany(agentId, rows);
1358
+ insertCount++;
1182
1359
  }
1183
- console.log(`[openclaw-memory-alibaba-local] clustered ${eventItems.length} event items \u2192 ${clusters.length} entries (max 3 per cluster, threshold 0.85)`);
1184
-
1185
- // World fact LRU GC: every 10 insertions
1186
- worldFactGcCounter++;
1360
+ }
1361
+ console.log(`[openclaw-memory-alibaba-local] worldImageExtraction done: ${worldActions.length} actions, ${insertCount} stored`);
1362
+
1363
+ // 5. World fact LRU GC: every 10 insertions
1364
+ if (insertCount > 0) {
1365
+ worldFactGcCounter += insertCount;
1187
1366
  if (worldFactGcCounter >= 10) {
1188
1367
  worldFactGcCounter = 0;
1189
1368
  db.gcWorldFact(agentId, 10_000, 30 * 24 * 60 * 60 * 1000, 25_000).catch((err) =>
1190
1369
  console.warn(`[openclaw-memory-alibaba-local] gcWorldFact failed: ${err}`),
1191
1370
  );
1192
1371
  }
1193
- } else {
1194
- // Simple dedup path for event items
1195
- for (const e of eventItems) {
1196
- const text = truncateForCapture(e.text, cfg.captureMaxChars);
1197
- if (await db.existsSemanticDuplicate(agentId, sessionKey, e.category, text)) {
1198
- continue;
1199
- }
1200
- await storeOneCaptureItem(agentId, { category: e.category, text, importance: e.importance }, cfg, db, backend, {
1201
- userId,
1202
- sessionId: sessionKey,
1203
- });
1204
- }
1205
1372
  }
1206
1373
  };
1207
1374
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-memory-alibaba-local",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "OpenClaw memory plugin: local LanceDB + DashScope-compatible embeddings",
5
5
  "type": "module",
6
6
  "engines": {
package/prompts.ts CHANGED
@@ -244,3 +244,60 @@ export function buildUserImageExtractionPrompt(
244
244
  `\nToday is ${today}.\n\nNew extractions:\n${newSection}\n\nExisting memories:\n${existingSection}\n`
245
245
  );
246
246
  }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // 4. WORLD IMAGE EXTRACTION (deduplicate & reconcile world facts)
250
+ // ---------------------------------------------------------------------------
251
+
252
+ export const WORLD_IMAGE_EXTRACTION_INSTRUCTIONS = `You are a World-Fact Organizer that keeps a concise, non-redundant knowledge base of real-world events, facts, and context mentioned in conversations.
253
+
254
+ # Inputs
255
+ - **Batch** (indexed 0..N-1): newly extracted world facts / events.
256
+ - **Store** (with id): existing world-fact memories in the database. May be empty.
257
+
258
+ # What to Keep
259
+ Only INSERT or UPDATE information that captures a concrete, verifiable fact or event:
260
+ 1. Events — what happened, when, where, who was involved
261
+ 2. Factual statements — statistics, dates, locations, outcomes
262
+ 3. Contextual knowledge — project status, external conditions, third-party decisions
263
+
264
+ # What to SKIP
265
+ - Information already fully covered by a Store item (same meaning, same or less detail)
266
+ - Vague or speculative statements with no concrete fact
267
+ - Purely conversational filler with no informational content
268
+
269
+ # Reconciliation Principles
270
+ 1. **Prefer the richer version**: When a batch item and a Store item describe the same topic, keep whichever has the most information. If the batch adds new details, UPDATE.
271
+ 2. **Preserve temporal markers**: Keep [as of ...] or [date] prefixes — world facts are time-sensitive.
272
+ 3. **High cohesion**: Only merge entries about the exact same event or fact. Different events stay separate even if related.
273
+ 4. **Contradiction = replace**: If a batch item directly contradicts a Store item (e.g. different outcome), DELETE the old item and INSERT the new one.
274
+
275
+ # Actions (one per batch index)
276
+ - **INSERT**: New world fact not in Store.
277
+ - **UPDATE** (memoryId): Store item covers the same event/fact; merge to produce the richer version.
278
+ - **SKIP**: Already fully covered by Store, or not a concrete fact.
279
+ - **DELETE** (memoryId): Batch item contradicts a Store item. Delete old; INSERT new.
280
+
281
+ # Output
282
+ Reply with ONLY a JSON object:
283
+ {"actions":[
284
+ {"index":0,"action":"insert","text":"[2026-04-01] Project X launched v2.0","importance":0.5},
285
+ {"index":1,"action":"skip"},
286
+ {"index":2,"action":"update","memoryId":"uuid","text":"[2026-03-28] Company Y acquired Z for $2B, deal finalized","importance":0.5},
287
+ {"index":3,"action":"delete","memoryId":"uuid"}
288
+ ]}
289
+ Every batch index must appear. A single index may produce BOTH delete + insert.
290
+ `;
291
+
292
+ export function buildWorldImageExtractionPrompt(
293
+ newItems: Array<{ index: number; text: string; importance: number }>,
294
+ existingMemories: Array<{ id: string; text: string }>,
295
+ ): string {
296
+ const today = new Date().toISOString().split("T")[0];
297
+ const newSection = JSON.stringify(newItems, null, 2);
298
+ const existingSection = JSON.stringify(existingMemories, null, 2);
299
+ return (
300
+ WORLD_IMAGE_EXTRACTION_INSTRUCTIONS +
301
+ `\nToday is ${today}.\n\nNew extractions:\n${newSection}\n\nExisting memories:\n${existingSection}\n`
302
+ );
303
+ }