memorylake-openclaw 1.0.0 → 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.
Files changed (3) hide show
  1. package/docs/openclaw.mdx +3 -3
  2. package/index.ts +203 -118
  3. package/package.json +1 -1
package/docs/openclaw.mdx CHANGED
@@ -54,7 +54,7 @@ The agent gets eight tools it can call during conversations:
54
54
  | `memory_forget` | Delete a memory by ID |
55
55
  | `document_search` | Search project documents for relevant paragraphs, tables, and figures |
56
56
  | `advanced_web_search` | Optional web search tool backed by the unified search API with plugin-level domain and locale constraints |
57
- | `open_data_search` | Optional search across open datasets — academic, clinical, drug, financial, economic, and more — routed to the appropriate proprietary data source based on the `dataset` field |
57
+ | `open_data_search` | Search across open datasets — academic, clinical, drug, financial, economic, and more — routed to the appropriate proprietary data source based on the `dataset` field |
58
58
 
59
59
  <Note>`open_data_search` requires the project to have at least one open data industry configured in MemoryLake. The `dataset` parameter is required and validated against the project's subscribed datasets at call time. The agent is automatically informed of available datasets via context injection at the start of each session. Supported datasets: `research/academic`, `clinical/trials`, `drug/database`, `financial/markets`, `company/fundamentals`, `economic/data`, `patents/ip`.</Note>
60
60
 
@@ -86,7 +86,7 @@ openclaw memorylake stats
86
86
  | `webSearchCountry` | `string` | — | Optional ISO country code for localizing `advanced_web_search` |
87
87
  | `webSearchTimezone` | `string` | — | Optional IANA timezone for localizing `advanced_web_search` |
88
88
 
89
- <Note>`advanced_web_search` and `open_data_search` are registered as optional OpenClaw tools, so they must be explicitly allowed before an agent can call them.</Note>
89
+ <Note>`advanced_web_search` is registered as an optional OpenClaw tool, so it must be explicitly allowed before an agent can call it.</Note>
90
90
 
91
91
  ## Key Features
92
92
 
@@ -98,7 +98,7 @@ openclaw memorylake stats
98
98
 
99
99
  ## Conclusion
100
100
 
101
- The `memorylake-openclaw` plugin gives OpenClaw agents persistent memory with minimal setup. Your agents can remember user preferences, facts, and context across sessions automatically — and optionally search across a wide range of open datasets when deeper external knowledge is needed.
101
+ The `memorylake-openclaw` plugin gives OpenClaw agents persistent memory with minimal setup. Your agents can remember user preferences, facts, and context across sessions automatically — and search across a wide range of open datasets when deeper external knowledge is needed.
102
102
 
103
103
  {/*<CardGroup cols={2}>
104
104
  <Card title="MemoryLake" icon="brain" href="https://app.memorylake.ai">
package/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Long-term memory via MemoryLake platform.
5
5
  *
6
6
  * Features:
7
- * - 8 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search, advanced_web_search, open_data_search
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> = {
@@ -569,11 +588,12 @@ function buildDocumentContext(
569
588
 
570
589
  for (const result of results) {
571
590
  const source = result.document_name ?? result.source_document?.file_name ?? "unknown";
591
+ const docId = result.document_id ?? "unknown";
572
592
  const highlight = result.highlight;
573
593
 
574
594
  if (result.type === "table") {
575
595
  const title = result.title || "Untitled Table";
576
- parts.push(`### Table: ${title} (from ${source})`);
596
+ parts.push(`### Table: ${title} (from ${source}, doc_id: ${docId})`);
577
597
  if (result.footnote) parts.push(`Note: ${result.footnote}`);
578
598
 
579
599
  for (const innerTable of highlight?.inner_tables ?? []) {
@@ -587,14 +607,14 @@ function buildDocumentContext(
587
607
  if (chunk.text) parts.push(chunk.text.slice(0, maxChunkLength));
588
608
  }
589
609
  } else if (result.type === "paragraph") {
590
- parts.push(`### Paragraph (from ${source}):`);
610
+ parts.push(`### Paragraph (from ${source}, doc_id: ${docId}):`);
591
611
  for (const chunk of highlight?.chunks ?? []) {
592
612
  if (chunk.text) parts.push(chunk.text.slice(0, maxChunkLength));
593
613
  }
594
614
  } else if (result.type === "figure") {
595
615
  const figure = highlight?.figure;
596
616
  if (figure) {
597
- parts.push(`### Figure (from ${source}):`);
617
+ parts.push(`### Figure (from ${source}, doc_id: ${docId}):`);
598
618
  if (figure.caption) parts.push(`Caption: ${figure.caption}`);
599
619
  const text = figure.text || figure.summary_text || "";
600
620
  if (text) parts.push(text);
@@ -820,6 +840,62 @@ const memoryPlugin = {
820
840
  // Cache project industries per session — fetched once, reused on subsequent prompts
821
841
  const sessionIndustriesCache = new Map<string, OpenDataIndustry[]>();
822
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
+
823
899
  api.logger.info(
824
900
  `memorylake-openclaw: registered (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, autoUpload: ${cfg.autoUpload})`,
825
901
  );
@@ -858,7 +934,7 @@ const memoryPlugin = {
858
934
  name: "memory_search",
859
935
  label: "Memory Search",
860
936
  description:
861
- "Search through long-term memories stored in MemoryLake. Use when you need context about user preferences, past decisions, or previously discussed topics.",
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.",
862
938
  parameters: Type.Object({
863
939
  query: Type.String({ description: "Search query" }),
864
940
  limit: Type.Optional(
@@ -893,54 +969,65 @@ const memoryPlugin = {
893
969
  scope?: "session" | "long-term" | "all";
894
970
  };
895
971
 
896
- try {
897
- const results = await effectiveProvider.search(
972
+ const [memoryResult, docResult] = await Promise.allSettled([
973
+ effectiveProvider.search(
898
974
  query,
899
975
  buildSearchOptions(effectiveCfg, userId, limit),
900
- );
976
+ ),
977
+ effectiveProvider.searchDocuments(query, effectiveCfg.topK),
978
+ ]);
901
979
 
902
- if (!results || results.length === 0) {
903
- return {
904
- content: [
905
- { type: "text", text: "No relevant memories found." },
906
- ],
907
- details: { count: 0 },
908
- };
909
- }
980
+ const sections: string[] = [];
981
+ let memoryCount = 0;
982
+ let docCount = 0;
983
+ let sanitizedMemories: { id: string; content: string; created_at: string }[] = [];
910
984
 
985
+ if (memoryResult.status === "fulfilled" && memoryResult.value.length > 0) {
986
+ const results = memoryResult.value;
987
+ memoryCount = results.length;
911
988
  const text = results
912
- .map(
913
- (r, i) =>
914
- `${i + 1}. ${r.content} (id: ${r.id})`,
915
- )
989
+ .map((r, i) => `${i + 1}. ${r.content} (id: ${r.id})`)
916
990
  .join("\n");
917
-
918
- const sanitized = results.map((r) => ({
991
+ sections.push(`## Memories\nFound ${results.length} memories:\n\n${text}`);
992
+ sanitizedMemories = results.map((r) => ({
919
993
  id: r.id,
920
994
  content: r.content,
921
995
  created_at: r.created_at,
922
996
  }));
997
+ } else if (memoryResult.status === "rejected") {
998
+ sections.push(`## Memories\nMemory search failed: ${String(memoryResult.reason)}`);
999
+ }
923
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) {
924
1010
  return {
925
1011
  content: [
926
- {
927
- type: "text",
928
- text: `Found ${results.length} memories:\n\n${text}`,
929
- },
930
- ],
931
- details: { count: results.length, memories: sanitized },
932
- };
933
- } catch (err) {
934
- return {
935
- content: [
936
- {
937
- type: "text",
938
- text: `Memory search failed: ${String(err)}`,
939
- },
1012
+ { type: "text", text: "No relevant memories or documents found." },
940
1013
  ],
941
- details: { error: String(err) },
1014
+ details: { count: 0 },
942
1015
  };
943
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
+ };
944
1031
  },
945
1032
  }),
946
1033
  { name: "memory_search" },
@@ -1233,6 +1320,55 @@ const memoryPlugin = {
1233
1320
  { name: "document_search" },
1234
1321
  );
1235
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
+
1236
1372
  api.registerTool(
1237
1373
  (ctx) => ({
1238
1374
  name: "advanced_web_search",
@@ -1506,7 +1642,6 @@ const memoryPlugin = {
1506
1642
  }
1507
1643
  },
1508
1644
  }),
1509
- { optional: true },
1510
1645
  );
1511
1646
 
1512
1647
  // ========================================================================
@@ -1811,7 +1946,9 @@ const memoryPlugin = {
1811
1946
  }
1812
1947
 
1813
1948
  // ------------------------------------------------------------------
1814
- // Auto-recall: inject relevant memories and documents before prompt build
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 [memoryResult, docResult] = await Promise.allSettled([
1858
- effectiveProvider.search(searchQuery, buildSearchOptions(effectiveCfg)),
1859
- effectiveProvider.searchDocuments(searchQuery, effectiveCfg.topK),
1860
- ]);
1861
-
1862
- const contextParts: string[] = [];
1863
-
1864
- if (memoryResult.status === "fulfilled" && memoryResult.value.length > 0) {
1865
- const memories = memoryResult.value;
1866
- const memoryContext = memories
1867
- .map((r) => `- ${r.content}`)
1868
- .join("\n");
1869
- contextParts.push(
1870
- `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
1871
- );
1872
- api.logger.info(
1873
- `memorylake-openclaw: injecting ${memories.length} memories into context`,
1874
- );
1875
-
1876
- // Fetch conflict details for memories flagged with unresolved conflicts
1877
- const conflictedIds = memories
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
- result.appendSystemContext =
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 (!result.prependContext && !result.appendSystemContext) return;
2015
+ if (appendParts.length > 0) {
2016
+ result.appendSystemContext = appendParts.join("\n\n");
2017
+ }
1933
2018
 
1934
2019
  return result;
1935
2020
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "1.0.0",
3
+ "version": "1.0.2-beta.1",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",