openclaw-memory-alibaba-local 1.0.4 → 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/db.ts +1 -1
- package/index.ts +228 -33
- package/package.json +1 -1
- package/prompts.ts +57 -0
package/db.ts
CHANGED
|
@@ -1400,7 +1400,7 @@ export class MemoryDB {
|
|
|
1400
1400
|
try {
|
|
1401
1401
|
const eid = sqlEscapeLiteral(id);
|
|
1402
1402
|
await (this.table! as any).update(
|
|
1403
|
-
{ createdAt: now },
|
|
1403
|
+
{ createdAt: String(now) },
|
|
1404
1404
|
{ where: `id = '${eid}' AND agentId = '${a}' AND category = '${wf}'` },
|
|
1405
1405
|
);
|
|
1406
1406
|
} catch (err) {
|
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;
|
|
@@ -947,6 +1061,11 @@ type DeltaFullContextRow = {
|
|
|
947
1061
|
/**
|
|
948
1062
|
* agent_end: per-role cursors → delta rows by source → LanceDB for full_context_* (shared batchId, no embed / no dedup);
|
|
949
1063
|
* then Promise.all(user-memory pipeline on user deltas, self-improving on user+assistant deltas).
|
|
1064
|
+
*
|
|
1065
|
+
* When `cursorOnly` is true the function advances the per-role cursor and persists it
|
|
1066
|
+
* but skips all memory extraction / storage. This is used for non-user triggers
|
|
1067
|
+
* (heartbeat, cron, memory, …) so the cursor stays in sync with the growing
|
|
1068
|
+
* session messages list without accidentally capturing heartbeat content.
|
|
950
1069
|
*/
|
|
951
1070
|
async function runAgentEndCapture(
|
|
952
1071
|
cfg: MemoryConfig,
|
|
@@ -957,6 +1076,7 @@ async function runAgentEndCapture(
|
|
|
957
1076
|
userId: string | null,
|
|
958
1077
|
messages: unknown[],
|
|
959
1078
|
lancedbDir: string,
|
|
1079
|
+
cursorOnly = false,
|
|
960
1080
|
): Promise<void> {
|
|
961
1081
|
if (messages.length === 0) {
|
|
962
1082
|
return;
|
|
@@ -973,6 +1093,25 @@ async function runAgentEndCapture(
|
|
|
973
1093
|
}
|
|
974
1094
|
|
|
975
1095
|
const running: Record<string, number> = { ...saved };
|
|
1096
|
+
|
|
1097
|
+
// --- cursor-only fast path: count roles then persist without extraction ---
|
|
1098
|
+
if (cursorOnly) {
|
|
1099
|
+
for (const msg of messages) {
|
|
1100
|
+
if (!msg || typeof msg !== "object") continue;
|
|
1101
|
+
const m = msg as Record<string, unknown>;
|
|
1102
|
+
const roleRaw = typeof m.role === "string" ? m.role : "unknown";
|
|
1103
|
+
const roleKey = normalizeRoleForCursor(roleRaw);
|
|
1104
|
+
running[roleKey] = (running[roleKey] ?? 0) + 1;
|
|
1105
|
+
}
|
|
1106
|
+
map[key] = { version: 2, roleCounts: { ...running }, lastMessagesLength: messages.length };
|
|
1107
|
+
saveAgentEndCursorMap(lancedbDir, map);
|
|
1108
|
+
console.log(
|
|
1109
|
+
`[openclaw-memory-alibaba-local] agent_end cursor-only advance (non-user trigger) messages=${messages.length}`,
|
|
1110
|
+
);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// --- full capture path (trigger === "user") ---
|
|
976
1115
|
const fullRows: DeltaFullContextRow[] = [];
|
|
977
1116
|
const userRawTexts: string[] = [];
|
|
978
1117
|
const uaLines: string[] = [];
|
|
@@ -1133,50 +1272,103 @@ async function captureUserMemoryFromInboundTexts(
|
|
|
1133
1272
|
// ---- Parallel: event-item pipeline & user-item pipeline ----
|
|
1134
1273
|
const eventPipeline = async () => {
|
|
1135
1274
|
if (eventItems.length === 0) return;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
for (const
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
+
});
|
|
1143
1287
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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);
|
|
1151
1342
|
const rows = buildChunkRows(
|
|
1152
|
-
{ category: WORLD_FACT as MemoryCategory, text, importance },
|
|
1343
|
+
{ category: WORLD_FACT as MemoryCategory, text: action.text, importance: action.importance },
|
|
1153
1344
|
vectors,
|
|
1154
1345
|
{ userId, sessionId: sessionKey },
|
|
1155
1346
|
);
|
|
1156
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++;
|
|
1157
1359
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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;
|
|
1162
1366
|
if (worldFactGcCounter >= 10) {
|
|
1163
1367
|
worldFactGcCounter = 0;
|
|
1164
1368
|
db.gcWorldFact(agentId, 10_000, 30 * 24 * 60 * 60 * 1000, 25_000).catch((err) =>
|
|
1165
1369
|
console.warn(`[openclaw-memory-alibaba-local] gcWorldFact failed: ${err}`),
|
|
1166
1370
|
);
|
|
1167
1371
|
}
|
|
1168
|
-
} else {
|
|
1169
|
-
// Simple dedup path for event items
|
|
1170
|
-
for (const e of eventItems) {
|
|
1171
|
-
const text = truncateForCapture(e.text, cfg.captureMaxChars);
|
|
1172
|
-
if (await db.existsSemanticDuplicate(agentId, sessionKey, e.category, text)) {
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
await storeOneCaptureItem(agentId, { category: e.category, text, importance: e.importance }, cfg, db, backend, {
|
|
1176
|
-
userId,
|
|
1177
|
-
sessionId: sessionKey,
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
1372
|
}
|
|
1181
1373
|
};
|
|
1182
1374
|
|
|
@@ -1942,7 +2134,6 @@ const memoryPlugin = {
|
|
|
1942
2134
|
|
|
1943
2135
|
if (cfg.autoCapture) {
|
|
1944
2136
|
api.on("agent_end", async (event, ctx) => {
|
|
1945
|
-
if ((ctx as { trigger?: string }).trigger !== "user") return;
|
|
1946
2137
|
if (!db || !backend) {
|
|
1947
2138
|
return;
|
|
1948
2139
|
}
|
|
@@ -1950,6 +2141,9 @@ const memoryPlugin = {
|
|
|
1950
2141
|
return;
|
|
1951
2142
|
}
|
|
1952
2143
|
|
|
2144
|
+
const trigger = (ctx as { trigger?: string }).trigger;
|
|
2145
|
+
const isUserTrigger = trigger === "user";
|
|
2146
|
+
|
|
1953
2147
|
try {
|
|
1954
2148
|
const tCap0 = Date.now();
|
|
1955
2149
|
const storageSessionKey = resolveStorageSessionKey(ctx);
|
|
@@ -1971,9 +2165,10 @@ const memoryPlugin = {
|
|
|
1971
2165
|
userId,
|
|
1972
2166
|
event.messages,
|
|
1973
2167
|
resolvedDbPath,
|
|
2168
|
+
!isUserTrigger,
|
|
1974
2169
|
);
|
|
1975
2170
|
console.log(
|
|
1976
|
-
`[openclaw-memory-alibaba-local] agent_end capture done totalHookMs=${Date.now() - tCap0} messages=${event.messages.length}`,
|
|
2171
|
+
`[openclaw-memory-alibaba-local] agent_end ${isUserTrigger ? "capture" : "cursor-only"} done totalHookMs=${Date.now() - tCap0} messages=${event.messages.length} trigger=${trigger ?? "unknown"}`,
|
|
1977
2172
|
);
|
|
1978
2173
|
} catch (err) {
|
|
1979
2174
|
console.warn(`[openclaw-memory-alibaba-local] agent_end capture failed: ${String(err)}`);
|
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
|
+
}
|