memorylake-openclaw 1.0.1 → 1.0.2-beta.2
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 +196 -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,52 @@ 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 get a temporary download URL. After calling this tool, you MUST call the `message` tool with action='send' and media=<the returned URL> to deliver the file to the user.",
|
|
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} ready. Download URL (expires in ~20 minutes):\n${downloadUrl}\n\nYou MUST now call the message tool with action="send" and media set to this URL to deliver the file to the user.`,
|
|
1349
|
+
},
|
|
1350
|
+
],
|
|
1351
|
+
details: { documentId },
|
|
1352
|
+
};
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
return {
|
|
1355
|
+
content: [
|
|
1356
|
+
{
|
|
1357
|
+
type: "text",
|
|
1358
|
+
text: `Document download failed: ${String(err)}`,
|
|
1359
|
+
},
|
|
1360
|
+
],
|
|
1361
|
+
details: { error: String(err) },
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
}),
|
|
1366
|
+
{ name: "document_download" },
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1237
1369
|
api.registerTool(
|
|
1238
1370
|
(ctx) => ({
|
|
1239
1371
|
name: "advanced_web_search",
|
|
@@ -1811,7 +1943,9 @@ const memoryPlugin = {
|
|
|
1811
1943
|
}
|
|
1812
1944
|
|
|
1813
1945
|
// ------------------------------------------------------------------
|
|
1814
|
-
// Auto-recall: inject
|
|
1946
|
+
// Auto-recall: inject system-level memory instructions and open data
|
|
1947
|
+
// categories before prompt build. Memory content is NOT pre-fetched;
|
|
1948
|
+
// the model is instructed to call memory_search itself.
|
|
1815
1949
|
// ------------------------------------------------------------------
|
|
1816
1950
|
if (cfg.autoRecall) {
|
|
1817
1951
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
@@ -1826,18 +1960,6 @@ const memoryPlugin = {
|
|
|
1826
1960
|
|
|
1827
1961
|
const sessionId = (ctx as any)?.sessionId ?? undefined;
|
|
1828
1962
|
|
|
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
1963
|
// Fetch industries once per session, then cache
|
|
1842
1964
|
let industries: OpenDataIndustry[] | undefined;
|
|
1843
1965
|
if (sessionId && sessionIndustriesCache.has(sessionId)) {
|
|
@@ -1854,82 +1976,42 @@ const memoryPlugin = {
|
|
|
1854
1976
|
}
|
|
1855
1977
|
}
|
|
1856
1978
|
|
|
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
|
-
|
|
1979
|
+
const result: { prependSystemContext?: string; appendSystemContext?: string } = {};
|
|
1980
|
+
|
|
1981
|
+
const memoryDirectiveParts: string[] = [
|
|
1982
|
+
`<memory-instructions>`,
|
|
1983
|
+
`You have access to the user's long-term memory AND documents via MemoryLake (memory_search tool).`,
|
|
1984
|
+
`The memory_search tool searches both memories and uploaded documents in a single call.`,
|
|
1985
|
+
``,
|
|
1986
|
+
`MANDATORY PROCEDURE for EVERY user message:`,
|
|
1987
|
+
`1. Read the user's message.`,
|
|
1988
|
+
`2. Call memory_search with a query derived from the message — this step is NOT optional.`,
|
|
1989
|
+
`3. Use the search results (memories + documents) to personalize your response.`,
|
|
1990
|
+
`4. Respond to the user.`,
|
|
1991
|
+
``,
|
|
1992
|
+
`This applies to ALL messages — greetings, recommendations, advice, casual chat, tasks, questions.`,
|
|
1993
|
+
`The user's preferences, history, name, hobbies, context, and documents are stored in MemoryLake.`,
|
|
1994
|
+
`A response without memory_search is INCORRECT because it ignores personalized context.`,
|
|
1995
|
+
`</memory-instructions>`,
|
|
1996
|
+
];
|
|
1997
|
+
result.prependSystemContext = memoryDirectiveParts.join("\n");
|
|
1998
|
+
|
|
1999
|
+
const appendParts: string[] = [];
|
|
1921
2000
|
if (industries && industries.length > 0) {
|
|
1922
2001
|
const categoryList = industries
|
|
1923
2002
|
.map((ind) => `- ${ind.id}: ${ind.name}${ind.description ? ` — ${ind.description}` : ""}`)
|
|
1924
2003
|
.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
|
|
2004
|
+
appendParts.push(
|
|
2005
|
+
`<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>`,
|
|
2006
|
+
);
|
|
1927
2007
|
api.logger.info(
|
|
1928
2008
|
`memorylake-openclaw: injecting ${industries.length} open data categories into system context`,
|
|
1929
2009
|
);
|
|
1930
2010
|
}
|
|
1931
2011
|
|
|
1932
|
-
if (
|
|
2012
|
+
if (appendParts.length > 0) {
|
|
2013
|
+
result.appendSystemContext = appendParts.join("\n\n");
|
|
2014
|
+
}
|
|
1933
2015
|
|
|
1934
2016
|
return result;
|
|
1935
2017
|
});
|