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.
- package/index.ts +198 -31
- package/package.json +1 -1
- 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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
for (const
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
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
|
+
}
|