memorylake-openclaw 1.0.1 → 1.0.2-beta.1
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 +199 -114
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Long-term memory via MemoryLake platform.
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* -
|
|
7
|
+
* - 9 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search, document_download, advanced_web_search, open_data_search
|
|
8
8
|
* - Auto-recall: injects relevant memories and document excerpts before each agent turn
|
|
9
9
|
* - Auto-capture: stores key facts scoped to the current session after each agent turn
|
|
10
10
|
* - CLI: openclaw memorylake search, openclaw memorylake stats
|
|
@@ -293,6 +293,7 @@ interface MemoryLakeProvider {
|
|
|
293
293
|
getAll(options: ListOptions): Promise<MemoryItem[]>;
|
|
294
294
|
delete(memoryId: string): Promise<void>;
|
|
295
295
|
searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse>;
|
|
296
|
+
getDocumentDownloadUrl(documentId: string): Promise<string>;
|
|
296
297
|
searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse>;
|
|
297
298
|
searchOpenData(query: string, options: OpenDataSearchOptions): Promise<OpenDataSearchResponse>;
|
|
298
299
|
getProject(): Promise<ProjectInfo>;
|
|
@@ -418,6 +419,24 @@ class PlatformProvider implements MemoryLakeProvider {
|
|
|
418
419
|
};
|
|
419
420
|
}
|
|
420
421
|
|
|
422
|
+
async getDocumentDownloadUrl(documentId: string): Promise<string> {
|
|
423
|
+
const downloadPath = `openapi/memorylake/api/v1/projects/${this.projectId}/documents/${documentId}/download`;
|
|
424
|
+
const resp = await this.http.get(downloadPath, {
|
|
425
|
+
followRedirect: false,
|
|
426
|
+
responseType: "text" as any,
|
|
427
|
+
throwHttpErrors: false,
|
|
428
|
+
});
|
|
429
|
+
if (resp.statusCode === 303 || resp.statusCode === 302) {
|
|
430
|
+
const location = resp.headers.location;
|
|
431
|
+
if (!location) throw new Error("Download redirect missing Location header");
|
|
432
|
+
return location;
|
|
433
|
+
}
|
|
434
|
+
if (resp.statusCode === 404) {
|
|
435
|
+
throw new Error(`Document not found: ${documentId}`);
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`Unexpected download response status: ${resp.statusCode}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
421
440
|
async searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse> {
|
|
422
441
|
const domain = options.domain != null ? normalizeWebSearchDomain(options.domain) : "web";
|
|
423
442
|
const body: Record<string, unknown> = {
|
|
@@ -821,6 +840,62 @@ const memoryPlugin = {
|
|
|
821
840
|
// Cache project industries per session — fetched once, reused on subsequent prompts
|
|
822
841
|
const sessionIndustriesCache = new Map<string, OpenDataIndustry[]>();
|
|
823
842
|
|
|
843
|
+
// ========================================================================
|
|
844
|
+
// System Prompt: Memory Section (injected into every system prompt)
|
|
845
|
+
// ========================================================================
|
|
846
|
+
api.registerMemoryPromptSection((params) => {
|
|
847
|
+
const lines: string[] = [
|
|
848
|
+
"## Memory (MANDATORY — MemoryLake)",
|
|
849
|
+
"",
|
|
850
|
+
"You have access to MemoryLake, a long-term memory system that stores the user's preferences, history, personal context, and uploaded documents across sessions.",
|
|
851
|
+
"",
|
|
852
|
+
];
|
|
853
|
+
|
|
854
|
+
if (params.availableTools.has("memory_search")) {
|
|
855
|
+
lines.push(
|
|
856
|
+
"### memory_search — MUST be called BEFORE every response",
|
|
857
|
+
"",
|
|
858
|
+
"**RULE: Your FIRST action for EVERY user message MUST be calling `memory_search`.** No exceptions.",
|
|
859
|
+
"",
|
|
860
|
+
"This tool searches BOTH memories AND documents in one call. It returns personal context (preferences, history, decisions) and relevant document excerpts (uploaded files, tables, figures).",
|
|
861
|
+
"",
|
|
862
|
+
"This applies to ALL types of questions, not just questions about memory or recall:",
|
|
863
|
+
"- Greetings -> search for who the user is",
|
|
864
|
+
"- Recommendations (books, activities, food) -> search for preferences and interests",
|
|
865
|
+
"- Advice (what to wear, what to eat) -> search for habits, location, preferences",
|
|
866
|
+
"- Tasks (write an email, self-introduction) -> search for user name, role, background",
|
|
867
|
+
"- Document questions -> search for relevant uploaded document content",
|
|
868
|
+
"- General chat -> search for recent context and ongoing topics",
|
|
869
|
+
"",
|
|
870
|
+
"Derive a broad query from the user's message. For example:",
|
|
871
|
+
"- User asks for a book recommendation -> search: reading preferences favorite books",
|
|
872
|
+
"- User asks about weekend plans -> search: hobbies interests weekend activities",
|
|
873
|
+
"- User says hello -> search: user name background recent context",
|
|
874
|
+
"",
|
|
875
|
+
"**If you respond without calling memory_search first, your response is WRONG.**",
|
|
876
|
+
"",
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (params.availableTools.has("memory_list")) {
|
|
881
|
+
lines.push(
|
|
882
|
+
"### memory_list",
|
|
883
|
+
"- When the user asks what you remember or wants to see all stored memories, call `memory_list`.",
|
|
884
|
+
"",
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (params.availableTools.has("memory_forget")) {
|
|
889
|
+
lines.push(
|
|
890
|
+
"### memory_forget",
|
|
891
|
+
"- When the user explicitly asks to delete or forget a specific memory, call `memory_forget` with the memory ID.",
|
|
892
|
+
"",
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return lines;
|
|
897
|
+
});
|
|
898
|
+
|
|
824
899
|
api.logger.info(
|
|
825
900
|
`memorylake-openclaw: registered (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, autoUpload: ${cfg.autoUpload})`,
|
|
826
901
|
);
|
|
@@ -859,7 +934,7 @@ const memoryPlugin = {
|
|
|
859
934
|
name: "memory_search",
|
|
860
935
|
label: "Memory Search",
|
|
861
936
|
description:
|
|
862
|
-
"Search through long-term memories stored in MemoryLake.
|
|
937
|
+
"MANDATORY: Search through long-term memories AND uploaded documents stored in MemoryLake. You MUST call this tool at the start of every conversation to recall the user's context, preferences, past decisions, previously discussed topics, and relevant document content. Always search before answering.",
|
|
863
938
|
parameters: Type.Object({
|
|
864
939
|
query: Type.String({ description: "Search query" }),
|
|
865
940
|
limit: Type.Optional(
|
|
@@ -894,54 +969,65 @@ const memoryPlugin = {
|
|
|
894
969
|
scope?: "session" | "long-term" | "all";
|
|
895
970
|
};
|
|
896
971
|
|
|
897
|
-
|
|
898
|
-
|
|
972
|
+
const [memoryResult, docResult] = await Promise.allSettled([
|
|
973
|
+
effectiveProvider.search(
|
|
899
974
|
query,
|
|
900
975
|
buildSearchOptions(effectiveCfg, userId, limit),
|
|
901
|
-
)
|
|
976
|
+
),
|
|
977
|
+
effectiveProvider.searchDocuments(query, effectiveCfg.topK),
|
|
978
|
+
]);
|
|
902
979
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
],
|
|
908
|
-
details: { count: 0 },
|
|
909
|
-
};
|
|
910
|
-
}
|
|
980
|
+
const sections: string[] = [];
|
|
981
|
+
let memoryCount = 0;
|
|
982
|
+
let docCount = 0;
|
|
983
|
+
let sanitizedMemories: { id: string; content: string; created_at: string }[] = [];
|
|
911
984
|
|
|
985
|
+
if (memoryResult.status === "fulfilled" && memoryResult.value.length > 0) {
|
|
986
|
+
const results = memoryResult.value;
|
|
987
|
+
memoryCount = results.length;
|
|
912
988
|
const text = results
|
|
913
|
-
.map(
|
|
914
|
-
(r, i) =>
|
|
915
|
-
`${i + 1}. ${r.content} (id: ${r.id})`,
|
|
916
|
-
)
|
|
989
|
+
.map((r, i) => `${i + 1}. ${r.content} (id: ${r.id})`)
|
|
917
990
|
.join("\n");
|
|
918
|
-
|
|
919
|
-
|
|
991
|
+
sections.push(`## Memories\nFound ${results.length} memories:\n\n${text}`);
|
|
992
|
+
sanitizedMemories = results.map((r) => ({
|
|
920
993
|
id: r.id,
|
|
921
994
|
content: r.content,
|
|
922
995
|
created_at: r.created_at,
|
|
923
996
|
}));
|
|
997
|
+
} else if (memoryResult.status === "rejected") {
|
|
998
|
+
sections.push(`## Memories\nMemory search failed: ${String(memoryResult.reason)}`);
|
|
999
|
+
}
|
|
924
1000
|
|
|
1001
|
+
if (docResult.status === "fulfilled" && docResult.value.results.length > 0) {
|
|
1002
|
+
docCount = docResult.value.results.length;
|
|
1003
|
+
const context = buildDocumentContext(docResult.value.results);
|
|
1004
|
+
sections.push(`## Documents\nFound ${docCount} document results:\n\n${context}`);
|
|
1005
|
+
} else if (docResult.status === "rejected") {
|
|
1006
|
+
sections.push(`## Documents\nDocument search failed: ${String(docResult.reason)}`);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (memoryCount === 0 && docCount === 0) {
|
|
925
1010
|
return {
|
|
926
1011
|
content: [
|
|
927
|
-
{
|
|
928
|
-
type: "text",
|
|
929
|
-
text: `Found ${results.length} memories:\n\n${text}`,
|
|
930
|
-
},
|
|
931
|
-
],
|
|
932
|
-
details: { count: results.length, memories: sanitized },
|
|
933
|
-
};
|
|
934
|
-
} catch (err) {
|
|
935
|
-
return {
|
|
936
|
-
content: [
|
|
937
|
-
{
|
|
938
|
-
type: "text",
|
|
939
|
-
text: `Memory search failed: ${String(err)}`,
|
|
940
|
-
},
|
|
1012
|
+
{ type: "text", text: "No relevant memories or documents found." },
|
|
941
1013
|
],
|
|
942
|
-
details: {
|
|
1014
|
+
details: { count: 0 },
|
|
943
1015
|
};
|
|
944
1016
|
}
|
|
1017
|
+
|
|
1018
|
+
return {
|
|
1019
|
+
content: [
|
|
1020
|
+
{
|
|
1021
|
+
type: "text",
|
|
1022
|
+
text: sections.join("\n\n"),
|
|
1023
|
+
},
|
|
1024
|
+
],
|
|
1025
|
+
details: {
|
|
1026
|
+
memoryCount,
|
|
1027
|
+
documentCount: docCount,
|
|
1028
|
+
memories: sanitizedMemories,
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
945
1031
|
},
|
|
946
1032
|
}),
|
|
947
1033
|
{ name: "memory_search" },
|
|
@@ -1234,6 +1320,55 @@ const memoryPlugin = {
|
|
|
1234
1320
|
{ name: "document_search" },
|
|
1235
1321
|
);
|
|
1236
1322
|
|
|
1323
|
+
api.registerTool(
|
|
1324
|
+
(ctx) => ({
|
|
1325
|
+
name: "document_download",
|
|
1326
|
+
label: "Document Download",
|
|
1327
|
+
description:
|
|
1328
|
+
"Download a document (image, PDF, etc.) from MemoryLake and send it to the user. Returns a temporary download URL. Use document_search first to find the document_id.",
|
|
1329
|
+
parameters: Type.Object({
|
|
1330
|
+
documentId: Type.String({
|
|
1331
|
+
description:
|
|
1332
|
+
"The document ID to download (from document_search results or document listing)",
|
|
1333
|
+
}),
|
|
1334
|
+
}),
|
|
1335
|
+
async execute(_toolCallId, params) {
|
|
1336
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
1337
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
1338
|
+
const { documentId } = params as { documentId: string };
|
|
1339
|
+
|
|
1340
|
+
try {
|
|
1341
|
+
const downloadUrl =
|
|
1342
|
+
await effectiveProvider.getDocumentDownloadUrl(documentId);
|
|
1343
|
+
|
|
1344
|
+
return {
|
|
1345
|
+
content: [
|
|
1346
|
+
{
|
|
1347
|
+
type: "text",
|
|
1348
|
+
text: `Document ${documentId} is ready. Tell the user you are sending the file now.\nMEDIA: ${downloadUrl}`,
|
|
1349
|
+
},
|
|
1350
|
+
],
|
|
1351
|
+
details: {
|
|
1352
|
+
media: { mediaUrl: downloadUrl },
|
|
1353
|
+
documentId,
|
|
1354
|
+
},
|
|
1355
|
+
};
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
return {
|
|
1358
|
+
content: [
|
|
1359
|
+
{
|
|
1360
|
+
type: "text",
|
|
1361
|
+
text: `Document download failed: ${String(err)}`,
|
|
1362
|
+
},
|
|
1363
|
+
],
|
|
1364
|
+
details: { error: String(err) },
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
},
|
|
1368
|
+
}),
|
|
1369
|
+
{ name: "document_download" },
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1237
1372
|
api.registerTool(
|
|
1238
1373
|
(ctx) => ({
|
|
1239
1374
|
name: "advanced_web_search",
|
|
@@ -1811,7 +1946,9 @@ const memoryPlugin = {
|
|
|
1811
1946
|
}
|
|
1812
1947
|
|
|
1813
1948
|
// ------------------------------------------------------------------
|
|
1814
|
-
// Auto-recall: inject
|
|
1949
|
+
// Auto-recall: inject system-level memory instructions and open data
|
|
1950
|
+
// categories before prompt build. Memory content is NOT pre-fetched;
|
|
1951
|
+
// the model is instructed to call memory_search itself.
|
|
1815
1952
|
// ------------------------------------------------------------------
|
|
1816
1953
|
if (cfg.autoRecall) {
|
|
1817
1954
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
@@ -1826,18 +1963,6 @@ const memoryPlugin = {
|
|
|
1826
1963
|
|
|
1827
1964
|
const sessionId = (ctx as any)?.sessionId ?? undefined;
|
|
1828
1965
|
|
|
1829
|
-
// LLM-rewrite FIRST — short prompts like "它呢?" can become meaningful
|
|
1830
|
-
// search queries when the LLM has conversation history context.
|
|
1831
|
-
const searchQuery = await rewriteQueryForSearch(event.prompt, event.messages, ctx);
|
|
1832
|
-
|
|
1833
|
-
// Only skip if the rewritten result is still too short
|
|
1834
|
-
if (searchQuery.length < 5) {
|
|
1835
|
-
api.logger.info(
|
|
1836
|
-
`memorylake-openclaw: skipping auto-recall, rewritten query too short (${searchQuery.length} chars)`,
|
|
1837
|
-
);
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
1966
|
// Fetch industries once per session, then cache
|
|
1842
1967
|
let industries: OpenDataIndustry[] | undefined;
|
|
1843
1968
|
if (sessionId && sessionIndustriesCache.has(sessionId)) {
|
|
@@ -1854,82 +1979,42 @@ const memoryPlugin = {
|
|
|
1854
1979
|
}
|
|
1855
1980
|
}
|
|
1856
1981
|
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
.filter((m) => m.has_unresolved_conflict)
|
|
1879
|
-
.map((m) => m.id);
|
|
1880
|
-
if (conflictedIds.length > 0) {
|
|
1881
|
-
try {
|
|
1882
|
-
const conflicts = await effectiveProvider.listConflicts(conflictedIds, effectiveCfg.userId);
|
|
1883
|
-
if (conflicts.length > 0) {
|
|
1884
|
-
const conflictContext = buildConflictContext(conflicts);
|
|
1885
|
-
contextParts.push(
|
|
1886
|
-
`<memory-conflicts>\nThe following conflicts exist among the recalled memories. ` +
|
|
1887
|
-
`Consider these contradictions when using the above memories.\n` +
|
|
1888
|
-
`If you have not already informed the user about these conflicts in this conversation, briefly mention that some recalled memories contain contradictions and note which points are uncertain. Do not repeat this notice if you have already done so.\n` +
|
|
1889
|
-
`${conflictContext}\n</memory-conflicts>`,
|
|
1890
|
-
);
|
|
1891
|
-
api.logger.info(
|
|
1892
|
-
`memorylake-openclaw: injecting ${conflicts.length} memory conflicts into context`,
|
|
1893
|
-
);
|
|
1894
|
-
}
|
|
1895
|
-
} catch (err) {
|
|
1896
|
-
api.logger.warn(`memorylake-openclaw: conflict fetch failed: ${String(err)}`);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
} else if (memoryResult.status === "rejected") {
|
|
1900
|
-
api.logger.warn(`memorylake-openclaw: memory recall failed: ${String(memoryResult.reason)}`);
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
if (docResult.status === "fulfilled" && docResult.value.results.length > 0) {
|
|
1904
|
-
const docContext = buildDocumentContext(docResult.value.results);
|
|
1905
|
-
contextParts.push(
|
|
1906
|
-
`<relevant-documents>\nThe following document excerpts may be relevant to this conversation:\n${docContext}\n</relevant-documents>`,
|
|
1907
|
-
);
|
|
1908
|
-
api.logger.info(
|
|
1909
|
-
`memorylake-openclaw: injecting ${docResult.value.results.length} document results into context`,
|
|
1910
|
-
);
|
|
1911
|
-
} else if (docResult.status === "rejected") {
|
|
1912
|
-
api.logger.warn(`memorylake-openclaw: document search failed: ${String(docResult.reason)}`);
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
const result: { prependContext?: string; appendSystemContext?: string } = {};
|
|
1916
|
-
|
|
1917
|
-
if (contextParts.length > 0) {
|
|
1918
|
-
result.prependContext = contextParts.join("\n\n");
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1982
|
+
const result: { prependSystemContext?: string; appendSystemContext?: string } = {};
|
|
1983
|
+
|
|
1984
|
+
const memoryDirectiveParts: string[] = [
|
|
1985
|
+
`<memory-instructions>`,
|
|
1986
|
+
`You have access to the user's long-term memory AND documents via MemoryLake (memory_search tool).`,
|
|
1987
|
+
`The memory_search tool searches both memories and uploaded documents in a single call.`,
|
|
1988
|
+
``,
|
|
1989
|
+
`MANDATORY PROCEDURE for EVERY user message:`,
|
|
1990
|
+
`1. Read the user's message.`,
|
|
1991
|
+
`2. Call memory_search with a query derived from the message — this step is NOT optional.`,
|
|
1992
|
+
`3. Use the search results (memories + documents) to personalize your response.`,
|
|
1993
|
+
`4. Respond to the user.`,
|
|
1994
|
+
``,
|
|
1995
|
+
`This applies to ALL messages — greetings, recommendations, advice, casual chat, tasks, questions.`,
|
|
1996
|
+
`The user's preferences, history, name, hobbies, context, and documents are stored in MemoryLake.`,
|
|
1997
|
+
`A response without memory_search is INCORRECT because it ignores personalized context.`,
|
|
1998
|
+
`</memory-instructions>`,
|
|
1999
|
+
];
|
|
2000
|
+
result.prependSystemContext = memoryDirectiveParts.join("\n");
|
|
2001
|
+
|
|
2002
|
+
const appendParts: string[] = [];
|
|
1921
2003
|
if (industries && industries.length > 0) {
|
|
1922
2004
|
const categoryList = industries
|
|
1923
2005
|
.map((ind) => `- ${ind.id}: ${ind.name}${ind.description ? ` — ${ind.description}` : ""}`)
|
|
1924
2006
|
.join("\n");
|
|
1925
|
-
|
|
1926
|
-
`<open-data-categories>\nThis project has access to the following open data categories via the open_data_search tool:\n${categoryList}\nWhen the user's question relates to any of these categories, use the open_data_search tool to retrieve relevant data.\n</open-data-categories
|
|
2007
|
+
appendParts.push(
|
|
2008
|
+
`<open-data-categories>\nThis project has access to the following open data categories via the open_data_search tool:\n${categoryList}\nWhen the user's question relates to any of these categories, use the open_data_search tool to retrieve relevant data.\n</open-data-categories>`,
|
|
2009
|
+
);
|
|
1927
2010
|
api.logger.info(
|
|
1928
2011
|
`memorylake-openclaw: injecting ${industries.length} open data categories into system context`,
|
|
1929
2012
|
);
|
|
1930
2013
|
}
|
|
1931
2014
|
|
|
1932
|
-
if (
|
|
2015
|
+
if (appendParts.length > 0) {
|
|
2016
|
+
result.appendSystemContext = appendParts.join("\n\n");
|
|
2017
|
+
}
|
|
1933
2018
|
|
|
1934
2019
|
return result;
|
|
1935
2020
|
});
|