memorylake-openclaw 1.0.2-beta.6 → 1.1.0
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/README.md +8 -6
- package/index.ts +22 -2202
- package/lib/cli/register-cli.ts +134 -0
- package/lib/config.ts +105 -0
- package/lib/helpers/parse-content-disposition.ts +21 -0
- package/lib/helpers/rewrite-query.ts +122 -0
- package/lib/helpers/upload-record.ts +47 -0
- package/lib/hooks/auto-capture.ts +97 -0
- package/lib/hooks/auto-recall.ts +89 -0
- package/lib/hooks/auto-upload.ts +72 -0
- package/lib/plugin-context.ts +76 -0
- package/lib/prompt/register-prompt.ts +66 -0
- package/lib/provider.ts +227 -0
- package/lib/tools/document-tools.ts +100 -0
- package/lib/tools/memory-tools.ts +298 -0
- package/lib/tools/search-tools.ts +288 -0
- package/lib/types.ts +272 -0
- package/lib/utils/builders.ts +125 -0
- package/lib/utils/normalizers.ts +76 -0
- package/openclaw.plugin.json +2 -0
- package/package.json +13 -3
- package/scripts/install.ps1 +37 -7
- package/scripts/install.sh +70 -5
- package/skills/memorylake-upload/SKILL.md +2 -2
- package/skills/memorylake-upload/scripts/upload.mjs +77 -28
- /package/{core-bridge.ts → lib/core-bridge.ts} +0 -0
package/index.ts
CHANGED
|
@@ -4,791 +4,24 @@
|
|
|
4
4
|
* Long-term memory via MemoryLake platform.
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* -
|
|
8
|
-
* - Auto-recall: injects
|
|
7
|
+
* - 7 tools: retrieve_context, memory_store, memory_list, memory_forget, document_download, advanced_web_search, open_data_search
|
|
8
|
+
* - Auto-recall: injects retrieve_context instructions (+ open-data categories) into system prompt and per-turn context
|
|
9
9
|
* - Auto-capture: stores key facts scoped to the current session after each agent turn
|
|
10
|
+
* - Auto-upload: uploads inbound files to MemoryLake as project documents
|
|
10
11
|
* - CLI: openclaw memorylake search, openclaw memorylake stats, openclaw memorylake upload
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import fs from "node:fs";
|
|
14
|
-
import fsPromises from "node:fs/promises";
|
|
15
|
-
import os from "node:os";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import got from "got";
|
|
18
|
-
import { pipeline } from "node:stream/promises";
|
|
19
|
-
import { Type } from "@sinclair/typebox";
|
|
20
14
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
projectId: string;
|
|
32
|
-
userId: string;
|
|
33
|
-
autoCapture: boolean;
|
|
34
|
-
autoRecall: boolean;
|
|
35
|
-
autoUpload: boolean;
|
|
36
|
-
searchThreshold: number;
|
|
37
|
-
topK: number;
|
|
38
|
-
rerank: boolean;
|
|
39
|
-
webSearchIncludeDomains?: string[];
|
|
40
|
-
webSearchExcludeDomains?: string[];
|
|
41
|
-
webSearchCountry?: string;
|
|
42
|
-
webSearchTimezone?: string;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// V2 API option types
|
|
46
|
-
interface AddOptions {
|
|
47
|
-
user_id: string;
|
|
48
|
-
chat_session_id?: string;
|
|
49
|
-
metadata?: Record<string, unknown>;
|
|
50
|
-
infer?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface SearchOptions {
|
|
54
|
-
user_id: string;
|
|
55
|
-
top_k?: number;
|
|
56
|
-
threshold?: number;
|
|
57
|
-
rerank?: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface ListOptions {
|
|
61
|
-
user_id?: string;
|
|
62
|
-
page?: number;
|
|
63
|
-
size?: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface MemoryItem {
|
|
67
|
-
id: string;
|
|
68
|
-
content: string;
|
|
69
|
-
user_id?: string;
|
|
70
|
-
created_at?: string;
|
|
71
|
-
updated_at?: string;
|
|
72
|
-
has_unresolved_conflict?: boolean;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface ConflictMemorySnapshot {
|
|
76
|
-
memory_id: string;
|
|
77
|
-
memory_history_id?: string;
|
|
78
|
-
memory_text: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface ConflictFileChunk {
|
|
82
|
-
chunk: { type?: string; text: string; range?: string };
|
|
83
|
-
document_id?: string;
|
|
84
|
-
document_name?: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface ConflictResolve {
|
|
88
|
-
id: string;
|
|
89
|
-
strategy: string;
|
|
90
|
-
keep_memory_id?: string;
|
|
91
|
-
forgotten_memory_ids?: string[];
|
|
92
|
-
resolved_by?: string;
|
|
93
|
-
created_at?: string;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface ConflictItem {
|
|
97
|
-
id: string;
|
|
98
|
-
name: string;
|
|
99
|
-
description: string;
|
|
100
|
-
category: "m2m" | "m2d";
|
|
101
|
-
conflict_type: "logical" | "knowledge";
|
|
102
|
-
memory_ids: string[];
|
|
103
|
-
memory_snapshots: ConflictMemorySnapshot[];
|
|
104
|
-
file_chunks: ConflictFileChunk[];
|
|
105
|
-
resolved: boolean;
|
|
106
|
-
resolve?: ConflictResolve;
|
|
107
|
-
stale?: boolean;
|
|
108
|
-
event_id?: string;
|
|
109
|
-
created_at?: string;
|
|
110
|
-
updated_at?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
interface ConflictListResponse {
|
|
114
|
-
items: ConflictItem[];
|
|
115
|
-
page: number;
|
|
116
|
-
total: number;
|
|
117
|
-
page_size: number;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
interface AddResultItem {
|
|
121
|
-
event_id: string;
|
|
122
|
-
status: string;
|
|
123
|
-
message: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
interface AddResult {
|
|
127
|
-
results: AddResultItem[];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
interface DocumentSearchResult {
|
|
131
|
-
type: "table" | "paragraph" | "figure";
|
|
132
|
-
document_id?: string;
|
|
133
|
-
document_name?: string;
|
|
134
|
-
source_document?: { file_name?: string };
|
|
135
|
-
highlight?: {
|
|
136
|
-
chunks?: Array<{ text?: string; range?: string }>;
|
|
137
|
-
inner_tables?: Array<{
|
|
138
|
-
id?: string;
|
|
139
|
-
columns?: Array<{ name?: string; data_type?: string }>;
|
|
140
|
-
num_rows?: number;
|
|
141
|
-
}>;
|
|
142
|
-
figure?: {
|
|
143
|
-
text?: string;
|
|
144
|
-
caption?: string;
|
|
145
|
-
summary_text?: string;
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
title?: string;
|
|
149
|
-
footnote?: string;
|
|
150
|
-
sheet_name?: string;
|
|
151
|
-
figure_id?: number;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
interface DocumentSearchResponse {
|
|
155
|
-
count: number;
|
|
156
|
-
results: DocumentSearchResult[];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Allowed values for web search domain (aligned with zootopia unified_search Domain).
|
|
161
|
-
* Declared as enum in schema; at runtime accept string and fall back to "auto" if invalid.
|
|
162
|
-
*/
|
|
163
|
-
const WebSearchDomainValues = [
|
|
164
|
-
"web",
|
|
165
|
-
"academic",
|
|
166
|
-
"news",
|
|
167
|
-
"people",
|
|
168
|
-
"company",
|
|
169
|
-
"financial",
|
|
170
|
-
"markets",
|
|
171
|
-
"code",
|
|
172
|
-
"legal",
|
|
173
|
-
"government",
|
|
174
|
-
"poi",
|
|
175
|
-
"auto",
|
|
176
|
-
] as const;
|
|
177
|
-
type WebSearchDomain = (typeof WebSearchDomainValues)[number];
|
|
178
|
-
|
|
179
|
-
const WEB_SEARCH_DOMAIN_SET = new Set<string>(WebSearchDomainValues);
|
|
180
|
-
|
|
181
|
-
/** Normalize domain: accept string at runtime; if not a valid enum value, return "auto". */
|
|
182
|
-
function normalizeWebSearchDomain(value: unknown): WebSearchDomain {
|
|
183
|
-
if (value == null) return "auto";
|
|
184
|
-
const s = typeof value === "string" ? value.toLowerCase().trim() : "";
|
|
185
|
-
return (WEB_SEARCH_DOMAIN_SET.has(s) ? s : "auto") as WebSearchDomain;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
interface WebSearchUserLocation {
|
|
189
|
-
country?: string;
|
|
190
|
-
timezone?: string;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
interface WebSearchOptions {
|
|
194
|
-
/** Declared as enum in schema; at runtime accept string, normalized with fallback to "auto". */
|
|
195
|
-
domain?: WebSearchDomain | string;
|
|
196
|
-
max_results?: number;
|
|
197
|
-
start_date?: string;
|
|
198
|
-
end_date?: string;
|
|
199
|
-
include_domains?: string[];
|
|
200
|
-
exclude_domains?: string[];
|
|
201
|
-
user_location?: WebSearchUserLocation;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
interface WebSearchResult {
|
|
205
|
-
url?: string;
|
|
206
|
-
title?: string;
|
|
207
|
-
summary?: string;
|
|
208
|
-
content?: string;
|
|
209
|
-
source?: string;
|
|
210
|
-
published_date?: string;
|
|
211
|
-
author?: string;
|
|
212
|
-
score?: number;
|
|
213
|
-
highlights?: string[];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
interface WebSearchResponse {
|
|
217
|
-
results: WebSearchResult[];
|
|
218
|
-
total_results: number;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Allowed values for open data search category (aligned with opendata endpoint).
|
|
223
|
-
* Maps to proprietary data sources per category.
|
|
224
|
-
*/
|
|
225
|
-
const OpenDataCategoryValues = [
|
|
226
|
-
"research/academic",
|
|
227
|
-
"clinical/trials",
|
|
228
|
-
"drug/database",
|
|
229
|
-
"financial/markets",
|
|
230
|
-
"company/fundamentals",
|
|
231
|
-
"economic/data",
|
|
232
|
-
"patents/ip",
|
|
233
|
-
] as const;
|
|
234
|
-
type OpenDataCategory = (typeof OpenDataCategoryValues)[number];
|
|
235
|
-
|
|
236
|
-
const OPEN_DATA_CATEGORY_SET = new Set<string>(OpenDataCategoryValues);
|
|
237
|
-
|
|
238
|
-
/** Normalize category: accept string at runtime; return undefined if not a valid enum value. */
|
|
239
|
-
function normalizeOpenDataCategory(value: unknown): OpenDataCategory | undefined {
|
|
240
|
-
if (value == null) return undefined;
|
|
241
|
-
const s = typeof value === "string" ? value.toLowerCase().trim() : "";
|
|
242
|
-
return OPEN_DATA_CATEGORY_SET.has(s) ? (s as OpenDataCategory) : undefined;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
interface OpenDataIndustry {
|
|
246
|
-
id: string;
|
|
247
|
-
name: string;
|
|
248
|
-
description?: string;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
interface ProjectInfo {
|
|
252
|
-
id: string;
|
|
253
|
-
name: string;
|
|
254
|
-
description?: string;
|
|
255
|
-
industries: OpenDataIndustry[];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
interface OpenDataSearchOptions {
|
|
259
|
-
dataset?: OpenDataCategory | string;
|
|
260
|
-
max_results?: number;
|
|
261
|
-
start_date?: string;
|
|
262
|
-
end_date?: string;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
interface OpenDataSearchResult {
|
|
266
|
-
title?: string;
|
|
267
|
-
url?: string;
|
|
268
|
-
summary?: string;
|
|
269
|
-
content?: string;
|
|
270
|
-
source?: string;
|
|
271
|
-
category?: string;
|
|
272
|
-
published_date?: string;
|
|
273
|
-
author?: string;
|
|
274
|
-
score?: number;
|
|
275
|
-
metadata?: Record<string, unknown>;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
interface OpenDataSearchResponse {
|
|
279
|
-
results: OpenDataSearchResult[];
|
|
280
|
-
total_results: number;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ============================================================================
|
|
284
|
-
// Unified Provider Interface
|
|
285
|
-
// ============================================================================
|
|
286
|
-
|
|
287
|
-
interface MemoryLakeProvider {
|
|
288
|
-
add(
|
|
289
|
-
messages: Array<{ role: string; content: string }>,
|
|
290
|
-
options: AddOptions,
|
|
291
|
-
): Promise<AddResult>;
|
|
292
|
-
search(query: string, options: SearchOptions): Promise<MemoryItem[]>;
|
|
293
|
-
get(memoryId: string): Promise<MemoryItem>;
|
|
294
|
-
getAll(options: ListOptions): Promise<MemoryItem[]>;
|
|
295
|
-
delete(memoryId: string): Promise<void>;
|
|
296
|
-
searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse>;
|
|
297
|
-
getDocumentDownloadUrl(documentId: string): Promise<string>;
|
|
298
|
-
searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse>;
|
|
299
|
-
searchOpenData(query: string, options: OpenDataSearchOptions): Promise<OpenDataSearchResponse>;
|
|
300
|
-
getProject(): Promise<ProjectInfo>;
|
|
301
|
-
listConflicts(memoryIds: string[], userId: string): Promise<ConflictItem[]>;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ============================================================================
|
|
305
|
-
// Platform Provider (MemoryLake Cloud)
|
|
306
|
-
// ============================================================================
|
|
307
|
-
|
|
308
|
-
interface ApiResponse<T = unknown> {
|
|
309
|
-
success: boolean;
|
|
310
|
-
message?: string;
|
|
311
|
-
data?: T;
|
|
312
|
-
error_code?: string;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
class PlatformProvider implements MemoryLakeProvider {
|
|
316
|
-
private readonly http: ReturnType<typeof got.extend>;
|
|
317
|
-
private readonly basePath: string;
|
|
318
|
-
private readonly docSearchPath: string;
|
|
319
|
-
private readonly webSearchPath: string;
|
|
320
|
-
private readonly openDataSearchPath: string;
|
|
321
|
-
private readonly projectPath: string;
|
|
322
|
-
private readonly conflictsPath: string;
|
|
323
|
-
private readonly projectId: string;
|
|
324
|
-
|
|
325
|
-
constructor(host: string, apiKey: string, projectId: string) {
|
|
326
|
-
this.projectId = projectId;
|
|
327
|
-
this.basePath = `openapi/memorylake/api/v2/projects/${projectId}/memories`;
|
|
328
|
-
this.docSearchPath = `openapi/memorylake/api/v1/projects/${projectId}/documents/search`;
|
|
329
|
-
this.webSearchPath = "openapi/memorylake/api/v1/search";
|
|
330
|
-
this.openDataSearchPath = "openapi/memorylake/api/v1/search/opendata";
|
|
331
|
-
this.projectPath = `openapi/memorylake/api/v1/projects/${projectId}`;
|
|
332
|
-
this.conflictsPath = `openapi/memorylake/api/v2/projects/${projectId}/memories/conflicts`;
|
|
333
|
-
this.http = got.extend({
|
|
334
|
-
prefixUrl: host,
|
|
335
|
-
headers: {
|
|
336
|
-
Authorization: `Bearer ${apiKey}`,
|
|
337
|
-
},
|
|
338
|
-
responseType: "json",
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async add(
|
|
343
|
-
messages: Array<{ role: string; content: string }>,
|
|
344
|
-
options: AddOptions,
|
|
345
|
-
): Promise<AddResult> {
|
|
346
|
-
const body: Record<string, unknown> = {
|
|
347
|
-
messages,
|
|
348
|
-
user_id: options.user_id,
|
|
349
|
-
infer: options.infer ?? true,
|
|
350
|
-
};
|
|
351
|
-
if (options.chat_session_id) body.chat_session_id = options.chat_session_id;
|
|
352
|
-
if (options.metadata) body.metadata = options.metadata;
|
|
353
|
-
|
|
354
|
-
const resp = await this.http
|
|
355
|
-
.post(this.basePath, { json: body })
|
|
356
|
-
.json<ApiResponse>();
|
|
357
|
-
if (!resp.success) throw new Error(resp.message ?? "add failed");
|
|
358
|
-
return normalizeAddResult(resp.data);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
|
|
362
|
-
const body: Record<string, unknown> = {
|
|
363
|
-
query,
|
|
364
|
-
user_id: options.user_id,
|
|
365
|
-
with_conflicts: true,
|
|
366
|
-
};
|
|
367
|
-
if (options.top_k != null) body.top_k = options.top_k;
|
|
368
|
-
if (options.threshold != null) body.threshold = options.threshold;
|
|
369
|
-
if (options.rerank != null) body.rerank = options.rerank;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const resp = await this.http
|
|
373
|
-
.post(`${this.basePath}/search`, { json: body })
|
|
374
|
-
.json<ApiResponse>();
|
|
375
|
-
if (!resp.success) throw new Error(resp.message ?? "search failed");
|
|
376
|
-
|
|
377
|
-
return normalizeSearchResults(resp.data);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async get(memoryId: string): Promise<MemoryItem> {
|
|
381
|
-
const resp = await this.http
|
|
382
|
-
.get(`${this.basePath}/${memoryId}`)
|
|
383
|
-
.json<ApiResponse>();
|
|
384
|
-
if (!resp.success) throw new Error(resp.message ?? "get failed");
|
|
385
|
-
return normalizeMemoryItem(resp.data);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async getAll(options: ListOptions): Promise<MemoryItem[]> {
|
|
389
|
-
const searchParams: Record<string, string | number> = {};
|
|
390
|
-
if (options.user_id) searchParams.user_id = options.user_id;
|
|
391
|
-
if (options.page != null) searchParams.page = options.page;
|
|
392
|
-
if (options.size != null) searchParams.size = options.size;
|
|
393
|
-
|
|
394
|
-
const resp = await this.http
|
|
395
|
-
.get(this.basePath, { searchParams })
|
|
396
|
-
.json<ApiResponse>();
|
|
397
|
-
if (!resp.success) throw new Error(resp.message ?? "getAll failed");
|
|
398
|
-
const data = resp.data as any;
|
|
399
|
-
if (data?.items && Array.isArray(data.items))
|
|
400
|
-
return data.items.map(normalizeMemoryItem);
|
|
401
|
-
return [];
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async delete(memoryId: string): Promise<void> {
|
|
405
|
-
const resp = await this.http
|
|
406
|
-
.delete(`${this.basePath}/${memoryId}`)
|
|
407
|
-
.json<ApiResponse>();
|
|
408
|
-
if (!resp.success) throw new Error(resp.message ?? "delete failed");
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse> {
|
|
412
|
-
const resp = await this.http
|
|
413
|
-
.post(this.docSearchPath, { json: { query, top_N: topN } })
|
|
414
|
-
.json<ApiResponse>();
|
|
415
|
-
if (!resp.success) throw new Error(resp.message ?? "document search failed");
|
|
416
|
-
const data = resp.data as any;
|
|
417
|
-
return {
|
|
418
|
-
count: data?.count ?? 0,
|
|
419
|
-
results: Array.isArray(data?.results) ? data.results : [],
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async getDocumentDownloadUrl(documentId: string): Promise<string> {
|
|
424
|
-
const downloadPath = `openapi/memorylake/api/v1/projects/${this.projectId}/documents/${documentId}/download`;
|
|
425
|
-
const resp = await this.http.get(downloadPath, {
|
|
426
|
-
followRedirect: false,
|
|
427
|
-
responseType: "text" as any,
|
|
428
|
-
throwHttpErrors: false,
|
|
429
|
-
});
|
|
430
|
-
if (resp.statusCode === 303 || resp.statusCode === 302) {
|
|
431
|
-
const location = resp.headers.location;
|
|
432
|
-
if (!location) throw new Error("Download redirect missing Location header");
|
|
433
|
-
return location;
|
|
434
|
-
}
|
|
435
|
-
if (resp.statusCode === 404) {
|
|
436
|
-
throw new Error(`Document not found: ${documentId}`);
|
|
437
|
-
}
|
|
438
|
-
throw new Error(`Unexpected download response status: ${resp.statusCode}`);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse> {
|
|
442
|
-
const domain = options.domain != null ? normalizeWebSearchDomain(options.domain) : "web";
|
|
443
|
-
const body: Record<string, unknown> = {
|
|
444
|
-
query,
|
|
445
|
-
domain,
|
|
446
|
-
};
|
|
447
|
-
if (options.max_results != null) body.max_results = options.max_results;
|
|
448
|
-
if (options.start_date) body.start_date = options.start_date;
|
|
449
|
-
if (options.end_date) body.end_date = options.end_date;
|
|
450
|
-
if (options.include_domains?.length) body.include_domains = options.include_domains;
|
|
451
|
-
if (options.exclude_domains?.length) body.exclude_domains = options.exclude_domains;
|
|
452
|
-
if (options.user_location) body.user_location = options.user_location;
|
|
453
|
-
|
|
454
|
-
const resp = await this.http
|
|
455
|
-
.post(this.webSearchPath, { json: body })
|
|
456
|
-
.json<WebSearchResponse>();
|
|
457
|
-
return normalizeWebSearchResponse(resp);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async searchOpenData(query: string, options: OpenDataSearchOptions): Promise<OpenDataSearchResponse> {
|
|
461
|
-
const body: Record<string, unknown> = { query };
|
|
462
|
-
if (options.dataset != null) {
|
|
463
|
-
const ds = normalizeOpenDataCategory(options.dataset);
|
|
464
|
-
if (!ds) throw new Error(`Invalid open data dataset: "${options.dataset}"`);
|
|
465
|
-
body.dataset = ds;
|
|
466
|
-
}
|
|
467
|
-
if (options.max_results != null) body.max_results = options.max_results;
|
|
468
|
-
if (options.start_date) body.start_date = options.start_date;
|
|
469
|
-
if (options.end_date) body.end_date = options.end_date;
|
|
470
|
-
|
|
471
|
-
const resp = await this.http
|
|
472
|
-
.post(this.openDataSearchPath, { json: body })
|
|
473
|
-
.json<OpenDataSearchResponse>();
|
|
474
|
-
return normalizeOpenDataSearchResponse(resp);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async getProject(): Promise<ProjectInfo> {
|
|
478
|
-
const resp = await this.http
|
|
479
|
-
.get(this.projectPath)
|
|
480
|
-
.json<ApiResponse<{ id?: string; name?: string; description?: string; industries?: Array<{ id?: string; name?: string; description?: string }> }>>();
|
|
481
|
-
if (!resp.success) throw new Error(resp.message ?? "get project failed");
|
|
482
|
-
const data = resp.data;
|
|
483
|
-
const info: ProjectInfo = {
|
|
484
|
-
id: data?.id ?? "",
|
|
485
|
-
name: data?.name ?? "",
|
|
486
|
-
description: data?.description,
|
|
487
|
-
industries: Array.isArray(data?.industries)
|
|
488
|
-
? data.industries.map((ind) => ({
|
|
489
|
-
id: ind.id ?? "",
|
|
490
|
-
name: ind.name ?? "",
|
|
491
|
-
description: ind.description,
|
|
492
|
-
}))
|
|
493
|
-
: [],
|
|
494
|
-
};
|
|
495
|
-
return info;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async listConflicts(memoryIds: string[], userId: string): Promise<ConflictItem[]> {
|
|
499
|
-
if (memoryIds.length === 0) return [];
|
|
500
|
-
const searchParams: Record<string, string> = {
|
|
501
|
-
resolved: "false",
|
|
502
|
-
memory_ids: memoryIds.join(","),
|
|
503
|
-
};
|
|
504
|
-
const resp = await this.http
|
|
505
|
-
.get(this.conflictsPath, {
|
|
506
|
-
searchParams
|
|
507
|
-
})
|
|
508
|
-
.json<ApiResponse<ConflictListResponse>>();
|
|
509
|
-
if (!resp.success) throw new Error(resp.message ?? "list conflicts failed");
|
|
510
|
-
const data = resp.data;
|
|
511
|
-
return Array.isArray(data?.items) ? data.items : [];
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// ============================================================================
|
|
517
|
-
// Result Normalizers
|
|
518
|
-
// ============================================================================
|
|
519
|
-
|
|
520
|
-
function normalizeMemoryItem(raw: any): MemoryItem {
|
|
521
|
-
return {
|
|
522
|
-
id: raw.id ?? "",
|
|
523
|
-
content: raw.content ?? "",
|
|
524
|
-
user_id: raw.user_id,
|
|
525
|
-
created_at: raw.created_at,
|
|
526
|
-
updated_at: raw.updated_at,
|
|
527
|
-
has_unresolved_conflict: raw.has_unresolved_conflict ?? false,
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function normalizeSearchResults(raw: any): MemoryItem[] {
|
|
532
|
-
if (raw?.results && Array.isArray(raw.results))
|
|
533
|
-
return raw.results.map(normalizeMemoryItem);
|
|
534
|
-
if (Array.isArray(raw)) return raw.map(normalizeMemoryItem);
|
|
535
|
-
return [];
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function normalizeAddResult(raw: any): AddResult {
|
|
539
|
-
const items = raw?.results ?? (Array.isArray(raw) ? raw : []);
|
|
540
|
-
return {
|
|
541
|
-
results: items.map((r: any) => ({
|
|
542
|
-
event_id: r.event_id ?? "",
|
|
543
|
-
status: r.status ?? "",
|
|
544
|
-
message: r.message ?? "",
|
|
545
|
-
})),
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function normalizeWebSearchResponse(raw: any): WebSearchResponse {
|
|
550
|
-
return {
|
|
551
|
-
results: Array.isArray(raw?.results) ? raw.results : [],
|
|
552
|
-
total_results: typeof raw?.total_results === "number" ? raw.total_results : 0,
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function normalizeOpenDataResult(raw: any): OpenDataSearchResult {
|
|
557
|
-
return {
|
|
558
|
-
title: typeof raw?.title === "string" ? raw.title : undefined,
|
|
559
|
-
url: typeof raw?.url === "string" ? raw.url : undefined,
|
|
560
|
-
summary: typeof raw?.summary === "string" ? raw.summary : undefined,
|
|
561
|
-
content: typeof raw?.content === "string" ? raw.content : undefined,
|
|
562
|
-
source: typeof raw?.source === "string" ? raw.source : undefined,
|
|
563
|
-
category: typeof raw?.category === "string" ? raw.category : undefined,
|
|
564
|
-
published_date: typeof raw?.published_date === "string" ? raw.published_date : undefined,
|
|
565
|
-
author: typeof raw?.author === "string" ? raw.author : undefined,
|
|
566
|
-
score: typeof raw?.score === "number" ? raw.score : undefined,
|
|
567
|
-
metadata: raw?.metadata && typeof raw.metadata === "object" && !Array.isArray(raw.metadata)
|
|
568
|
-
? raw.metadata as Record<string, unknown>
|
|
569
|
-
: undefined,
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function normalizeOpenDataSearchResponse(raw: any): OpenDataSearchResponse {
|
|
574
|
-
return {
|
|
575
|
-
results: Array.isArray(raw?.results) ? raw.results.map(normalizeOpenDataResult) : [],
|
|
576
|
-
total_results: typeof raw?.total_results === "number" ? raw.total_results : 0,
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// ============================================================================
|
|
581
|
-
// Document Context Builder
|
|
582
|
-
// ============================================================================
|
|
583
|
-
|
|
584
|
-
function buildDocumentContext(
|
|
585
|
-
results: DocumentSearchResult[],
|
|
586
|
-
maxChunkLength = 10000,
|
|
587
|
-
): string {
|
|
588
|
-
const parts: string[] = [];
|
|
589
|
-
|
|
590
|
-
for (const result of results) {
|
|
591
|
-
const source = result.document_name ?? result.source_document?.file_name ?? "unknown";
|
|
592
|
-
const docId = result.document_id ?? "unknown";
|
|
593
|
-
const highlight = result.highlight;
|
|
594
|
-
|
|
595
|
-
if (result.type === "table") {
|
|
596
|
-
const title = result.title || "Untitled Table";
|
|
597
|
-
parts.push(`### Table: ${title} (from ${source}, doc_id: ${docId})`);
|
|
598
|
-
if (result.footnote) parts.push(`Note: ${result.footnote}`);
|
|
599
|
-
|
|
600
|
-
for (const innerTable of highlight?.inner_tables ?? []) {
|
|
601
|
-
const colDesc = (innerTable.columns ?? [])
|
|
602
|
-
.map((c) => `${c.name}(${c.data_type})`)
|
|
603
|
-
.join(", ");
|
|
604
|
-
if (colDesc) parts.push(`Columns: ${colDesc}`);
|
|
605
|
-
if (innerTable.num_rows != null) parts.push(`Rows: ${innerTable.num_rows}`);
|
|
606
|
-
}
|
|
607
|
-
for (const chunk of highlight?.chunks ?? []) {
|
|
608
|
-
if (chunk.text) parts.push(chunk.text.slice(0, maxChunkLength));
|
|
609
|
-
}
|
|
610
|
-
} else if (result.type === "paragraph") {
|
|
611
|
-
parts.push(`### Paragraph (from ${source}, doc_id: ${docId}):`);
|
|
612
|
-
for (const chunk of highlight?.chunks ?? []) {
|
|
613
|
-
if (chunk.text) parts.push(chunk.text.slice(0, maxChunkLength));
|
|
614
|
-
}
|
|
615
|
-
} else if (result.type === "figure") {
|
|
616
|
-
const figure = highlight?.figure;
|
|
617
|
-
if (figure) {
|
|
618
|
-
parts.push(`### Figure (from ${source}, doc_id: ${docId}):`);
|
|
619
|
-
if (figure.caption) parts.push(`Caption: ${figure.caption}`);
|
|
620
|
-
const text = figure.text || figure.summary_text || "";
|
|
621
|
-
if (text) parts.push(text);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
return parts.join("\n\n");
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function buildWebSearchContext(results: WebSearchResult[]): string {
|
|
630
|
-
return results
|
|
631
|
-
.map((result, index) => {
|
|
632
|
-
const parts = [`${index + 1}. ${result.title ?? result.url ?? "Untitled result"}`];
|
|
633
|
-
if (result.url) parts.push(`URL: ${result.url}`);
|
|
634
|
-
if (result.summary) parts.push(`Summary: ${result.summary}`);
|
|
635
|
-
if (result.source) parts.push(`Source: ${result.source}`);
|
|
636
|
-
if (result.published_date) parts.push(`Published: ${result.published_date}`);
|
|
637
|
-
return parts.join("\n");
|
|
638
|
-
})
|
|
639
|
-
.join("\n\n");
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function buildConflictContext(conflicts: ConflictItem[], maxChunkLength = 200): string {
|
|
643
|
-
return conflicts
|
|
644
|
-
.map((c) => {
|
|
645
|
-
const parts: string[] = [
|
|
646
|
-
`- [${c.conflict_type}] ${c.description}`,
|
|
647
|
-
];
|
|
648
|
-
for (const snap of c.memory_snapshots ?? []) {
|
|
649
|
-
parts.push(` Memory(${snap.memory_id}): ${snap.memory_text.slice(0, maxChunkLength)}`);
|
|
650
|
-
}
|
|
651
|
-
for (const fc of c.file_chunks ?? []) {
|
|
652
|
-
const docLabel = fc.document_name ?? fc.document_id ?? "unknown";
|
|
653
|
-
parts.push(` Document(${docLabel}): ${fc.chunk.text.slice(0, maxChunkLength)}`);
|
|
654
|
-
}
|
|
655
|
-
return parts.join("\n");
|
|
656
|
-
})
|
|
657
|
-
.join("\n");
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function buildOpenDataContext(results: OpenDataSearchResult[]): string {
|
|
661
|
-
const filtered = results.map((r) => {
|
|
662
|
-
const item: Record<string, unknown> = {};
|
|
663
|
-
if (r.title != null) item.title = r.title;
|
|
664
|
-
if (r.url != null) item.url = r.url;
|
|
665
|
-
if (r.content != null) item.content = r.content;
|
|
666
|
-
if (r.published_date != null) item.published_date = r.published_date;
|
|
667
|
-
if (r.category != null) item.category = r.category;
|
|
668
|
-
return item;
|
|
669
|
-
});
|
|
670
|
-
return JSON.stringify(filtered, null, 2);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// ============================================================================
|
|
674
|
-
// Config Parser
|
|
675
|
-
// ============================================================================
|
|
676
|
-
|
|
677
|
-
// ============================================================================
|
|
678
|
-
// Config Schema
|
|
679
|
-
// ============================================================================
|
|
680
|
-
|
|
681
|
-
const ALLOWED_KEYS = [
|
|
682
|
-
"host",
|
|
683
|
-
"apiKey",
|
|
684
|
-
"projectId",
|
|
685
|
-
"userId",
|
|
686
|
-
"autoCapture",
|
|
687
|
-
"autoRecall",
|
|
688
|
-
"autoUpload",
|
|
689
|
-
"searchThreshold",
|
|
690
|
-
"topK",
|
|
691
|
-
"rerank",
|
|
692
|
-
"webSearchIncludeDomains",
|
|
693
|
-
"webSearchExcludeDomains",
|
|
694
|
-
"webSearchCountry",
|
|
695
|
-
"webSearchTimezone",
|
|
696
|
-
];
|
|
697
|
-
|
|
698
|
-
function assertAllowedKeys(
|
|
699
|
-
value: Record<string, unknown>,
|
|
700
|
-
allowed: string[],
|
|
701
|
-
label: string,
|
|
702
|
-
) {
|
|
703
|
-
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
704
|
-
if (unknown.length === 0) return;
|
|
705
|
-
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function parseOptionalStringArray(
|
|
709
|
-
value: unknown,
|
|
710
|
-
label: string,
|
|
711
|
-
): string[] | undefined {
|
|
712
|
-
if (value == null) return undefined;
|
|
713
|
-
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
714
|
-
throw new Error(`${label} must be an array of strings`);
|
|
715
|
-
}
|
|
716
|
-
return value;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function parseOptionalString(
|
|
720
|
-
value: unknown,
|
|
721
|
-
label: string,
|
|
722
|
-
): string | undefined {
|
|
723
|
-
if (value == null) return undefined;
|
|
724
|
-
if (typeof value !== "string") {
|
|
725
|
-
throw new Error(`${label} must be a string`);
|
|
726
|
-
}
|
|
727
|
-
return value;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const memoryLakeConfigSchema = {
|
|
731
|
-
parse(value: unknown): MemoryLakeConfig {
|
|
732
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
733
|
-
throw new Error("memorylake-openclaw config required");
|
|
734
|
-
}
|
|
735
|
-
const cfg = value as Record<string, unknown>;
|
|
736
|
-
assertAllowedKeys(cfg, ALLOWED_KEYS, "memorylake-openclaw config");
|
|
737
|
-
|
|
738
|
-
if (typeof cfg.apiKey !== "string" || !cfg.apiKey) {
|
|
739
|
-
throw new Error("apiKey is required");
|
|
740
|
-
}
|
|
741
|
-
if (typeof cfg.projectId !== "string" || !cfg.projectId) {
|
|
742
|
-
throw new Error("projectId is required");
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
return {
|
|
746
|
-
host:
|
|
747
|
-
typeof cfg.host === "string" && cfg.host
|
|
748
|
-
? cfg.host
|
|
749
|
-
: "https://app.memorylake.ai",
|
|
750
|
-
apiKey: cfg.apiKey as string,
|
|
751
|
-
projectId: cfg.projectId as string,
|
|
752
|
-
userId: DEFAULT_USER_ID,
|
|
753
|
-
autoCapture: cfg.autoCapture !== false,
|
|
754
|
-
autoRecall: cfg.autoRecall !== false,
|
|
755
|
-
autoUpload: cfg.autoUpload !== false,
|
|
756
|
-
searchThreshold:
|
|
757
|
-
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.3,
|
|
758
|
-
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
|
759
|
-
rerank: cfg.rerank !== false,
|
|
760
|
-
webSearchIncludeDomains: parseOptionalStringArray(
|
|
761
|
-
cfg.webSearchIncludeDomains,
|
|
762
|
-
"webSearchIncludeDomains",
|
|
763
|
-
),
|
|
764
|
-
webSearchExcludeDomains: parseOptionalStringArray(
|
|
765
|
-
cfg.webSearchExcludeDomains,
|
|
766
|
-
"webSearchExcludeDomains",
|
|
767
|
-
),
|
|
768
|
-
webSearchCountry: parseOptionalString(
|
|
769
|
-
cfg.webSearchCountry,
|
|
770
|
-
"webSearchCountry",
|
|
771
|
-
),
|
|
772
|
-
webSearchTimezone: parseOptionalString(
|
|
773
|
-
cfg.webSearchTimezone,
|
|
774
|
-
"webSearchTimezone",
|
|
775
|
-
),
|
|
776
|
-
};
|
|
777
|
-
},
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
// ============================================================================
|
|
781
|
-
// Plugin Definition
|
|
782
|
-
// ============================================================================
|
|
783
|
-
|
|
784
|
-
/** Shared type for the upload / uploadAuto function signature */
|
|
785
|
-
type UploadFn = (opts: {
|
|
786
|
-
host: string;
|
|
787
|
-
apiKey: string;
|
|
788
|
-
projectId: string;
|
|
789
|
-
filePath: string;
|
|
790
|
-
fileName: string;
|
|
791
|
-
}) => Promise<unknown>;
|
|
15
|
+
import { memoryLakeConfigSchema } from "./lib/config";
|
|
16
|
+
import { createPluginContext } from "./lib/plugin-context";
|
|
17
|
+
import { registerMemoryPromptSection } from "./lib/prompt/register-prompt";
|
|
18
|
+
import { registerMemoryTools } from "./lib/tools/memory-tools";
|
|
19
|
+
import { registerDocumentTools } from "./lib/tools/document-tools";
|
|
20
|
+
import { registerSearchTools } from "./lib/tools/search-tools";
|
|
21
|
+
import { registerCli } from "./lib/cli/register-cli";
|
|
22
|
+
import { registerAutoUpload } from "./lib/hooks/auto-upload";
|
|
23
|
+
import { registerAutoRecall } from "./lib/hooks/auto-recall";
|
|
24
|
+
import { registerAutoCapture } from "./lib/hooks/auto-capture";
|
|
792
25
|
|
|
793
26
|
const memoryPlugin = {
|
|
794
27
|
id: "memorylake-openclaw",
|
|
@@ -799,1434 +32,21 @@ const memoryPlugin = {
|
|
|
799
32
|
|
|
800
33
|
register(api: OpenClawPluginApi) {
|
|
801
34
|
const cfg = memoryLakeConfigSchema.parse(api.pluginConfig);
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
// Provider cache: avoids re-creating providers for the same host+apiKey+projectId
|
|
805
|
-
const providerCache = new Map<string, MemoryLakeProvider>();
|
|
806
|
-
const globalProviderKey = `${cfg.host}|${cfg.apiKey}|${cfg.projectId}`;
|
|
807
|
-
providerCache.set(globalProviderKey, provider);
|
|
808
|
-
|
|
809
|
-
function getProvider(effectiveCfg: MemoryLakeConfig): MemoryLakeProvider {
|
|
810
|
-
const key = `${effectiveCfg.host}|${effectiveCfg.apiKey}|${effectiveCfg.projectId}`;
|
|
811
|
-
let p = providerCache.get(key);
|
|
812
|
-
if (!p) {
|
|
813
|
-
p = new PlatformProvider(effectiveCfg.host, effectiveCfg.apiKey, effectiveCfg.projectId);
|
|
814
|
-
providerCache.set(key, p);
|
|
815
|
-
}
|
|
816
|
-
return p;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function resolveConfig(ctx: any): MemoryLakeConfig {
|
|
820
|
-
const workspaceDir = ctx?.workspaceDir;
|
|
821
|
-
if (!workspaceDir) return cfg;
|
|
822
|
-
|
|
823
|
-
const localPath = path.join(workspaceDir, ".memorylake", "config.json");
|
|
824
|
-
if (!fs.existsSync(localPath)) return cfg;
|
|
825
|
-
|
|
826
|
-
try {
|
|
827
|
-
const raw = JSON.parse(fs.readFileSync(localPath, "utf-8"));
|
|
828
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
829
|
-
api.logger.warn(
|
|
830
|
-
`memorylake-openclaw: workspace config exists but is not a JSON object; falling back to global config (path: ${localPath})`,
|
|
831
|
-
);
|
|
832
|
-
return cfg;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const allowed = new Set(ALLOWED_KEYS);
|
|
836
|
-
const overrides: Record<string, unknown> = {};
|
|
837
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
838
|
-
if (allowed.has(key)) overrides[key] = value;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return { ...cfg, ...overrides } as MemoryLakeConfig;
|
|
842
|
-
} catch (err) {
|
|
843
|
-
api.logger.warn(
|
|
844
|
-
`memorylake-openclaw: failed to parse workspace config JSON; falling back to global config (path: ${localPath}): ${String(err)}`,
|
|
845
|
-
);
|
|
846
|
-
return cfg;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// Cache project industries per session — fetched once, reused on subsequent prompts
|
|
851
|
-
const sessionIndustriesCache = new Map<string, OpenDataIndustry[]>();
|
|
35
|
+
const pctx = createPluginContext(api, cfg);
|
|
852
36
|
|
|
853
|
-
|
|
854
|
-
// System Prompt: Memory Section (injected into every system prompt)
|
|
855
|
-
// ========================================================================
|
|
856
|
-
api.registerMemoryPromptSection((params) => {
|
|
857
|
-
const lines: string[] = [
|
|
858
|
-
"## Memory (MANDATORY — MemoryLake)",
|
|
859
|
-
"",
|
|
860
|
-
"You have access to MemoryLake, a long-term memory system that stores the user's preferences, history, personal context, and uploaded documents across sessions.",
|
|
861
|
-
"",
|
|
862
|
-
];
|
|
863
|
-
|
|
864
|
-
if (params.availableTools.has("memory_search")) {
|
|
865
|
-
lines.push(
|
|
866
|
-
"### memory_search — MUST be called BEFORE every response",
|
|
867
|
-
"",
|
|
868
|
-
"**RULE: Your FIRST action for EVERY user message MUST be calling `memory_search`.** No exceptions.",
|
|
869
|
-
"",
|
|
870
|
-
"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).",
|
|
871
|
-
"",
|
|
872
|
-
"This applies to ALL types of questions, not just questions about memory or recall:",
|
|
873
|
-
"- Greetings -> search for who the user is",
|
|
874
|
-
"- Recommendations (books, activities, food) -> search for preferences and interests",
|
|
875
|
-
"- Advice (what to wear, what to eat) -> search for habits, location, preferences",
|
|
876
|
-
"- Tasks (write an email, self-introduction) -> search for user name, role, background",
|
|
877
|
-
"- Document questions -> search for relevant uploaded document content",
|
|
878
|
-
"- General chat -> search for recent context and ongoing topics",
|
|
879
|
-
"",
|
|
880
|
-
"Derive a broad query from the user's message. For example:",
|
|
881
|
-
"- User asks for a book recommendation -> search: reading preferences favorite books",
|
|
882
|
-
"- User asks about weekend plans -> search: hobbies interests weekend activities",
|
|
883
|
-
"- User says hello -> search: user name background recent context",
|
|
884
|
-
"",
|
|
885
|
-
"**If you respond without calling memory_search first, your response is WRONG.**",
|
|
886
|
-
"",
|
|
887
|
-
);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
if (params.availableTools.has("memory_list")) {
|
|
891
|
-
lines.push(
|
|
892
|
-
"### memory_list",
|
|
893
|
-
"- When the user asks what you remember or wants to see all stored memories, call `memory_list`.",
|
|
894
|
-
"",
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if (params.availableTools.has("memory_forget")) {
|
|
899
|
-
lines.push(
|
|
900
|
-
"### memory_forget",
|
|
901
|
-
"- When the user explicitly asks to delete or forget a specific memory, call `memory_forget` with the memory ID.",
|
|
902
|
-
"",
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
return lines;
|
|
907
|
-
});
|
|
37
|
+
registerMemoryPromptSection(pctx, cfg);
|
|
908
38
|
|
|
909
39
|
api.logger.info(
|
|
910
40
|
`memorylake-openclaw: registered (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, autoUpload: ${cfg.autoUpload})`,
|
|
911
41
|
);
|
|
912
42
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if (sessionId) opts.chat_session_id = sessionId;
|
|
921
|
-
return opts;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Helper: build search options
|
|
925
|
-
function buildSearchOptions(
|
|
926
|
-
effectiveCfg: MemoryLakeConfig,
|
|
927
|
-
userIdOverride?: string,
|
|
928
|
-
limit?: number,
|
|
929
|
-
): SearchOptions {
|
|
930
|
-
return {
|
|
931
|
-
user_id: userIdOverride || effectiveCfg.userId,
|
|
932
|
-
top_k: limit ?? effectiveCfg.topK,
|
|
933
|
-
threshold: effectiveCfg.searchThreshold,
|
|
934
|
-
rerank: effectiveCfg.rerank,
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// ========================================================================
|
|
939
|
-
// Tools
|
|
940
|
-
// ========================================================================
|
|
941
|
-
|
|
942
|
-
api.registerTool(
|
|
943
|
-
(ctx) => ({
|
|
944
|
-
name: "memory_search",
|
|
945
|
-
label: "Memory Search",
|
|
946
|
-
description:
|
|
947
|
-
"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.",
|
|
948
|
-
parameters: Type.Object({
|
|
949
|
-
query: Type.String({ description: "Search query" }),
|
|
950
|
-
limit: Type.Optional(
|
|
951
|
-
Type.Number({
|
|
952
|
-
description: `Max results (default: ${cfg.topK})`,
|
|
953
|
-
}),
|
|
954
|
-
),
|
|
955
|
-
userId: Type.Optional(
|
|
956
|
-
Type.String({
|
|
957
|
-
description:
|
|
958
|
-
"User ID to scope search (default: configured userId)",
|
|
959
|
-
}),
|
|
960
|
-
),
|
|
961
|
-
scope: Type.Optional(
|
|
962
|
-
Type.Union([
|
|
963
|
-
Type.Literal("session"),
|
|
964
|
-
Type.Literal("long-term"),
|
|
965
|
-
Type.Literal("all"),
|
|
966
|
-
], {
|
|
967
|
-
description:
|
|
968
|
-
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
|
969
|
-
}),
|
|
970
|
-
),
|
|
971
|
-
}),
|
|
972
|
-
async execute(_toolCallId, params) {
|
|
973
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
974
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
975
|
-
const { query, limit, userId, scope = "all" } = params as {
|
|
976
|
-
query: string;
|
|
977
|
-
limit?: number;
|
|
978
|
-
userId?: string;
|
|
979
|
-
scope?: "session" | "long-term" | "all";
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
const [memoryResult, docResult] = await Promise.allSettled([
|
|
983
|
-
effectiveProvider.search(
|
|
984
|
-
query,
|
|
985
|
-
buildSearchOptions(effectiveCfg, userId, limit),
|
|
986
|
-
),
|
|
987
|
-
effectiveProvider.searchDocuments(query, effectiveCfg.topK),
|
|
988
|
-
]);
|
|
989
|
-
|
|
990
|
-
const sections: string[] = [];
|
|
991
|
-
let memoryCount = 0;
|
|
992
|
-
let docCount = 0;
|
|
993
|
-
let sanitizedMemories: { id: string; content: string; created_at: string }[] = [];
|
|
994
|
-
|
|
995
|
-
if (memoryResult.status === "fulfilled" && memoryResult.value.length > 0) {
|
|
996
|
-
const results = memoryResult.value;
|
|
997
|
-
memoryCount = results.length;
|
|
998
|
-
const text = results
|
|
999
|
-
.map((r, i) => `${i + 1}. ${r.content} (id: ${r.id})`)
|
|
1000
|
-
.join("\n");
|
|
1001
|
-
sections.push(`## Memories\nFound ${results.length} memories:\n\n${text}`);
|
|
1002
|
-
sanitizedMemories = results.map((r) => ({
|
|
1003
|
-
id: r.id,
|
|
1004
|
-
content: r.content,
|
|
1005
|
-
created_at: r.created_at,
|
|
1006
|
-
}));
|
|
1007
|
-
} else if (memoryResult.status === "rejected") {
|
|
1008
|
-
sections.push(`## Memories\nMemory search failed: ${String(memoryResult.reason)}`);
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
if (docResult.status === "fulfilled" && docResult.value.results.length > 0) {
|
|
1012
|
-
docCount = docResult.value.results.length;
|
|
1013
|
-
const context = buildDocumentContext(docResult.value.results);
|
|
1014
|
-
sections.push(`## Documents\nFound ${docCount} document results:\n\n${context}`);
|
|
1015
|
-
} else if (docResult.status === "rejected") {
|
|
1016
|
-
sections.push(`## Documents\nDocument search failed: ${String(docResult.reason)}`);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (memoryCount === 0 && docCount === 0) {
|
|
1020
|
-
return {
|
|
1021
|
-
content: [
|
|
1022
|
-
{ type: "text", text: "No relevant memories or documents found." },
|
|
1023
|
-
],
|
|
1024
|
-
details: { count: 0 },
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
return {
|
|
1029
|
-
content: [
|
|
1030
|
-
{
|
|
1031
|
-
type: "text",
|
|
1032
|
-
text: sections.join("\n\n"),
|
|
1033
|
-
},
|
|
1034
|
-
],
|
|
1035
|
-
details: {
|
|
1036
|
-
memoryCount,
|
|
1037
|
-
documentCount: docCount,
|
|
1038
|
-
memories: sanitizedMemories,
|
|
1039
|
-
},
|
|
1040
|
-
};
|
|
1041
|
-
},
|
|
1042
|
-
}),
|
|
1043
|
-
{ name: "memory_search" },
|
|
1044
|
-
);
|
|
1045
|
-
|
|
1046
|
-
api.registerTool(
|
|
1047
|
-
(ctx) => ({
|
|
1048
|
-
name: "memory_store",
|
|
1049
|
-
label: "Memory Store",
|
|
1050
|
-
description:
|
|
1051
|
-
"Save important information in long-term memory via MemoryLake. Use for preferences, facts, decisions, and anything worth remembering.",
|
|
1052
|
-
parameters: Type.Object({
|
|
1053
|
-
text: Type.String({ description: "Information to remember" }),
|
|
1054
|
-
userId: Type.Optional(
|
|
1055
|
-
Type.String({
|
|
1056
|
-
description: "User ID to scope this memory",
|
|
1057
|
-
}),
|
|
1058
|
-
),
|
|
1059
|
-
metadata: Type.Optional(
|
|
1060
|
-
Type.Record(Type.String(), Type.Unknown(), {
|
|
1061
|
-
description: "Optional metadata to attach to this memory",
|
|
1062
|
-
}),
|
|
1063
|
-
),
|
|
1064
|
-
}),
|
|
1065
|
-
async execute(_toolCallId, params) {
|
|
1066
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1067
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1068
|
-
const { text, userId } = params as {
|
|
1069
|
-
text: string;
|
|
1070
|
-
userId?: string;
|
|
1071
|
-
metadata?: Record<string, unknown>;
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
try {
|
|
1075
|
-
const result = await effectiveProvider.add(
|
|
1076
|
-
[{ role: "user", content: text }],
|
|
1077
|
-
buildAddOptions(effectiveCfg, userId, (ctx as any)?.sessionId),
|
|
1078
|
-
);
|
|
1079
|
-
|
|
1080
|
-
const count = result.results?.length ?? 0;
|
|
1081
|
-
|
|
1082
|
-
return {
|
|
1083
|
-
content: [
|
|
1084
|
-
{
|
|
1085
|
-
type: "text",
|
|
1086
|
-
text: count > 0
|
|
1087
|
-
? `Submitted ${count} memory task(s) for processing. ${result.results.map((r) => `[${r.status}] ${r.message}`).join("; ")}`
|
|
1088
|
-
: "No memories extracted.",
|
|
1089
|
-
},
|
|
1090
|
-
],
|
|
1091
|
-
details: {
|
|
1092
|
-
action: "stored",
|
|
1093
|
-
results: result.results,
|
|
1094
|
-
},
|
|
1095
|
-
};
|
|
1096
|
-
} catch (err) {
|
|
1097
|
-
return {
|
|
1098
|
-
content: [
|
|
1099
|
-
{
|
|
1100
|
-
type: "text",
|
|
1101
|
-
text: `Memory store failed: ${String(err)}`,
|
|
1102
|
-
},
|
|
1103
|
-
],
|
|
1104
|
-
details: { error: String(err) },
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
},
|
|
1108
|
-
}),
|
|
1109
|
-
{ name: "memory_store" },
|
|
1110
|
-
);
|
|
1111
|
-
|
|
1112
|
-
api.registerTool(
|
|
1113
|
-
(ctx) => ({
|
|
1114
|
-
name: "memory_get",
|
|
1115
|
-
label: "Memory Get",
|
|
1116
|
-
description: "Retrieve a specific memory by its ID from MemoryLake.",
|
|
1117
|
-
parameters: Type.Object({
|
|
1118
|
-
memoryId: Type.String({ description: "The memory ID to retrieve" }),
|
|
1119
|
-
}),
|
|
1120
|
-
async execute(_toolCallId, params) {
|
|
1121
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1122
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1123
|
-
const { memoryId } = params as { memoryId: string };
|
|
1124
|
-
|
|
1125
|
-
try {
|
|
1126
|
-
const memory = await effectiveProvider.get(memoryId);
|
|
1127
|
-
|
|
1128
|
-
return {
|
|
1129
|
-
content: [
|
|
1130
|
-
{
|
|
1131
|
-
type: "text",
|
|
1132
|
-
text: `Memory ${memory.id}:\n${memory.content}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}`,
|
|
1133
|
-
},
|
|
1134
|
-
],
|
|
1135
|
-
details: { memory },
|
|
1136
|
-
};
|
|
1137
|
-
} catch (err) {
|
|
1138
|
-
return {
|
|
1139
|
-
content: [
|
|
1140
|
-
{
|
|
1141
|
-
type: "text",
|
|
1142
|
-
text: `Memory get failed: ${String(err)}`,
|
|
1143
|
-
},
|
|
1144
|
-
],
|
|
1145
|
-
details: { error: String(err) },
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
},
|
|
1149
|
-
}),
|
|
1150
|
-
{ name: "memory_get" },
|
|
1151
|
-
);
|
|
1152
|
-
|
|
1153
|
-
api.registerTool(
|
|
1154
|
-
(ctx) => ({
|
|
1155
|
-
name: "memory_list",
|
|
1156
|
-
label: "Memory List",
|
|
1157
|
-
description:
|
|
1158
|
-
"List all stored memories for a user. Use this when you want to see everything that's been remembered, rather than searching for something specific.",
|
|
1159
|
-
parameters: Type.Object({
|
|
1160
|
-
userId: Type.Optional(
|
|
1161
|
-
Type.String({
|
|
1162
|
-
description:
|
|
1163
|
-
"User ID to list memories for (default: configured userId)",
|
|
1164
|
-
}),
|
|
1165
|
-
),
|
|
1166
|
-
scope: Type.Optional(
|
|
1167
|
-
Type.Union([
|
|
1168
|
-
Type.Literal("session"),
|
|
1169
|
-
Type.Literal("long-term"),
|
|
1170
|
-
Type.Literal("all"),
|
|
1171
|
-
], {
|
|
1172
|
-
description:
|
|
1173
|
-
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
|
1174
|
-
}),
|
|
1175
|
-
),
|
|
1176
|
-
}),
|
|
1177
|
-
async execute(_toolCallId, params) {
|
|
1178
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1179
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1180
|
-
const { userId, scope = "all" } = params as { userId?: string; scope?: "session" | "long-term" | "all" };
|
|
1181
|
-
|
|
1182
|
-
try {
|
|
1183
|
-
const uid = userId || effectiveCfg.userId;
|
|
1184
|
-
const memories = await effectiveProvider.getAll({ user_id: uid });
|
|
1185
|
-
|
|
1186
|
-
if (!memories || memories.length === 0) {
|
|
1187
|
-
return {
|
|
1188
|
-
content: [
|
|
1189
|
-
{ type: "text", text: "No memories stored yet." },
|
|
1190
|
-
],
|
|
1191
|
-
details: { count: 0 },
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const text = memories
|
|
1196
|
-
.map(
|
|
1197
|
-
(r, i) =>
|
|
1198
|
-
`${i + 1}. ${r.content} (id: ${r.id})`,
|
|
1199
|
-
)
|
|
1200
|
-
.join("\n");
|
|
1201
|
-
|
|
1202
|
-
const sanitized = memories.map((r) => ({
|
|
1203
|
-
id: r.id,
|
|
1204
|
-
content: r.content,
|
|
1205
|
-
created_at: r.created_at,
|
|
1206
|
-
}));
|
|
1207
|
-
|
|
1208
|
-
return {
|
|
1209
|
-
content: [
|
|
1210
|
-
{
|
|
1211
|
-
type: "text",
|
|
1212
|
-
text: `${memories.length} memories:\n\n${text}`,
|
|
1213
|
-
},
|
|
1214
|
-
],
|
|
1215
|
-
details: { count: memories.length, memories: sanitized },
|
|
1216
|
-
};
|
|
1217
|
-
} catch (err) {
|
|
1218
|
-
return {
|
|
1219
|
-
content: [
|
|
1220
|
-
{
|
|
1221
|
-
type: "text",
|
|
1222
|
-
text: `Memory list failed: ${String(err)}`,
|
|
1223
|
-
},
|
|
1224
|
-
],
|
|
1225
|
-
details: { error: String(err) },
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
},
|
|
1229
|
-
}),
|
|
1230
|
-
{ name: "memory_list" },
|
|
1231
|
-
);
|
|
1232
|
-
|
|
1233
|
-
api.registerTool(
|
|
1234
|
-
(ctx) => ({
|
|
1235
|
-
name: "memory_forget",
|
|
1236
|
-
label: "Memory Forget",
|
|
1237
|
-
description:
|
|
1238
|
-
"Delete a specific memory by ID from MemoryLake.",
|
|
1239
|
-
parameters: Type.Object({
|
|
1240
|
-
memoryId: Type.String({ description: "Memory ID to delete" }),
|
|
1241
|
-
}),
|
|
1242
|
-
async execute(_toolCallId, params) {
|
|
1243
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1244
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1245
|
-
const { memoryId } = params as { memoryId: string };
|
|
1246
|
-
|
|
1247
|
-
try {
|
|
1248
|
-
await effectiveProvider.delete(memoryId);
|
|
1249
|
-
return {
|
|
1250
|
-
content: [
|
|
1251
|
-
{ type: "text", text: `Memory ${memoryId} forgotten.` },
|
|
1252
|
-
],
|
|
1253
|
-
details: { action: "deleted", id: memoryId },
|
|
1254
|
-
};
|
|
1255
|
-
} catch (err) {
|
|
1256
|
-
return {
|
|
1257
|
-
content: [
|
|
1258
|
-
{
|
|
1259
|
-
type: "text",
|
|
1260
|
-
text: `Memory forget failed: ${String(err)}`,
|
|
1261
|
-
},
|
|
1262
|
-
],
|
|
1263
|
-
details: { error: String(err) },
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
},
|
|
1267
|
-
}),
|
|
1268
|
-
{ name: "memory_forget" },
|
|
1269
|
-
);
|
|
1270
|
-
|
|
1271
|
-
api.registerTool(
|
|
1272
|
-
(ctx) => ({
|
|
1273
|
-
name: "document_search",
|
|
1274
|
-
label: "Document Search",
|
|
1275
|
-
description:
|
|
1276
|
-
"Search through documents stored in MemoryLake project. Returns relevant paragraphs, tables, and figures from uploaded documents.",
|
|
1277
|
-
parameters: Type.Object({
|
|
1278
|
-
query: Type.String({ description: "Search query" }),
|
|
1279
|
-
topN: Type.Optional(
|
|
1280
|
-
Type.Number({
|
|
1281
|
-
description: `Max results (default: ${cfg.topK})`,
|
|
1282
|
-
minimum: 1,
|
|
1283
|
-
}),
|
|
1284
|
-
),
|
|
1285
|
-
}),
|
|
1286
|
-
async execute(_toolCallId, params) {
|
|
1287
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1288
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1289
|
-
const { query, topN } = params as { query: string; topN?: number };
|
|
1290
|
-
|
|
1291
|
-
try {
|
|
1292
|
-
const response = await effectiveProvider.searchDocuments(
|
|
1293
|
-
query,
|
|
1294
|
-
topN ?? effectiveCfg.topK,
|
|
1295
|
-
);
|
|
1296
|
-
|
|
1297
|
-
if (!response.results || response.results.length === 0) {
|
|
1298
|
-
return {
|
|
1299
|
-
content: [
|
|
1300
|
-
{ type: "text", text: "No relevant documents found." },
|
|
1301
|
-
],
|
|
1302
|
-
details: { count: 0 },
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
const context = buildDocumentContext(response.results);
|
|
1307
|
-
|
|
1308
|
-
return {
|
|
1309
|
-
content: [
|
|
1310
|
-
{
|
|
1311
|
-
type: "text",
|
|
1312
|
-
text: `Found ${response.results.length} document results:\n\n${context}`,
|
|
1313
|
-
},
|
|
1314
|
-
],
|
|
1315
|
-
details: { count: response.results.length },
|
|
1316
|
-
};
|
|
1317
|
-
} catch (err) {
|
|
1318
|
-
return {
|
|
1319
|
-
content: [
|
|
1320
|
-
{
|
|
1321
|
-
type: "text",
|
|
1322
|
-
text: `Document search failed: ${String(err)}`,
|
|
1323
|
-
},
|
|
1324
|
-
],
|
|
1325
|
-
details: { error: String(err) },
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
},
|
|
1329
|
-
}),
|
|
1330
|
-
{ name: "document_search" },
|
|
1331
|
-
);
|
|
1332
|
-
|
|
1333
|
-
/**
|
|
1334
|
-
* Try to extract a usable filename from a pre-signed download URL.
|
|
1335
|
-
* Returns null if the URL doesn't contain a recognizable filename.
|
|
1336
|
-
*/
|
|
1337
|
-
function fileNameFromUrl(urlStr: string): string | null {
|
|
1338
|
-
try {
|
|
1339
|
-
const p = new URL(urlStr).pathname;
|
|
1340
|
-
const base = path.posix.basename(p);
|
|
1341
|
-
if (base && /\.\w{1,10}$/.test(base)) return decodeURIComponent(base);
|
|
1342
|
-
} catch { /* ignore */ }
|
|
1343
|
-
return null;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
api.registerTool(
|
|
1347
|
-
(ctx) => ({
|
|
1348
|
-
name: "document_download",
|
|
1349
|
-
label: "Document Download",
|
|
1350
|
-
description:
|
|
1351
|
-
"Download a document (image, PDF, etc.) from MemoryLake to local disk. After calling this tool, you MUST call the `message` tool with action='send' and media=<the returned local file path> to deliver the file to the user.",
|
|
1352
|
-
parameters: Type.Object({
|
|
1353
|
-
documentId: Type.String({
|
|
1354
|
-
description:
|
|
1355
|
-
"The document ID to download (from document_search results or document listing)",
|
|
1356
|
-
}),
|
|
1357
|
-
fileName: Type.Optional(
|
|
1358
|
-
Type.String({
|
|
1359
|
-
description:
|
|
1360
|
-
"Original file name for saving locally (e.g. 'report.pdf'). Falls back to the name in the download URL or the document ID.",
|
|
1361
|
-
}),
|
|
1362
|
-
),
|
|
1363
|
-
}),
|
|
1364
|
-
async execute(_toolCallId, params) {
|
|
1365
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1366
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1367
|
-
const { documentId, fileName } = params as {
|
|
1368
|
-
documentId: string;
|
|
1369
|
-
fileName?: string;
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
try {
|
|
1373
|
-
// 1. Get pre-signed download URL
|
|
1374
|
-
const downloadUrl =
|
|
1375
|
-
await effectiveProvider.getDocumentDownloadUrl(documentId);
|
|
1376
|
-
|
|
1377
|
-
// 2. Determine local save directory (cross-platform)
|
|
1378
|
-
const workspaceDir = (ctx as any)?.workspaceDir;
|
|
1379
|
-
const downloadDir = workspaceDir
|
|
1380
|
-
? path.join(workspaceDir, ".memorylake", "downloads")
|
|
1381
|
-
: path.join(os.tmpdir(), "memorylake-downloads");
|
|
1382
|
-
fs.mkdirSync(downloadDir, { recursive: true });
|
|
1383
|
-
|
|
1384
|
-
// 3. Determine filename: explicit param > URL-derived > documentId
|
|
1385
|
-
const finalName =
|
|
1386
|
-
fileName || fileNameFromUrl(downloadUrl) || documentId;
|
|
1387
|
-
const localPath = path.join(downloadDir, finalName);
|
|
1388
|
-
|
|
1389
|
-
// 4. Stream download to local file
|
|
1390
|
-
await pipeline(
|
|
1391
|
-
got.stream(downloadUrl),
|
|
1392
|
-
fs.createWriteStream(localPath),
|
|
1393
|
-
);
|
|
1394
|
-
|
|
1395
|
-
return {
|
|
1396
|
-
content: [
|
|
1397
|
-
{
|
|
1398
|
-
type: "text",
|
|
1399
|
-
text: `Document ${documentId} downloaded to:\n${localPath}\n\nYou MUST now call the message tool with action="send" and media set to this local path to deliver the file to the user.`,
|
|
1400
|
-
},
|
|
1401
|
-
],
|
|
1402
|
-
details: { documentId, localPath },
|
|
1403
|
-
};
|
|
1404
|
-
} catch (err) {
|
|
1405
|
-
return {
|
|
1406
|
-
content: [
|
|
1407
|
-
{
|
|
1408
|
-
type: "text",
|
|
1409
|
-
text: `Document download failed: ${String(err)}`,
|
|
1410
|
-
},
|
|
1411
|
-
],
|
|
1412
|
-
details: { error: String(err) },
|
|
1413
|
-
};
|
|
1414
|
-
}
|
|
1415
|
-
},
|
|
1416
|
-
}),
|
|
1417
|
-
{ name: "document_download" },
|
|
1418
|
-
);
|
|
1419
|
-
|
|
1420
|
-
api.registerTool(
|
|
1421
|
-
(ctx) => ({
|
|
1422
|
-
name: "advanced_web_search",
|
|
1423
|
-
label: "Advanced Web Search",
|
|
1424
|
-
description:
|
|
1425
|
-
"Search the web using the unified search API with plugin-level domain and location constraints. Use this for recent information, public web pages, or web research that should respect configured allowed domains, blocked domains, and user locale.",
|
|
1426
|
-
parameters: Type.Object({
|
|
1427
|
-
query: Type.String({
|
|
1428
|
-
description:
|
|
1429
|
-
"The web search query to send to the unified search endpoint.",
|
|
1430
|
-
}),
|
|
1431
|
-
domain: Type.Optional(
|
|
1432
|
-
Type.Union(
|
|
1433
|
-
[
|
|
1434
|
-
Type.Literal("web"),
|
|
1435
|
-
Type.Literal("academic"),
|
|
1436
|
-
Type.Literal("news"),
|
|
1437
|
-
Type.Literal("people"),
|
|
1438
|
-
Type.Literal("company"),
|
|
1439
|
-
Type.Literal("financial"),
|
|
1440
|
-
Type.Literal("markets"),
|
|
1441
|
-
Type.Literal("code"),
|
|
1442
|
-
Type.Literal("legal"),
|
|
1443
|
-
Type.Literal("government"),
|
|
1444
|
-
Type.Literal("poi"),
|
|
1445
|
-
Type.Literal("auto"),
|
|
1446
|
-
],
|
|
1447
|
-
{
|
|
1448
|
-
description:
|
|
1449
|
-
"Search domain. Default: web. Invalid or unknown values are treated as auto.",
|
|
1450
|
-
},
|
|
1451
|
-
),
|
|
1452
|
-
),
|
|
1453
|
-
maxResults: Type.Optional(
|
|
1454
|
-
Type.Number({
|
|
1455
|
-
description: `Maximum number of web results to return (default: ${cfg.topK}).`,
|
|
1456
|
-
minimum: 1,
|
|
1457
|
-
}),
|
|
1458
|
-
),
|
|
1459
|
-
startDate: Type.Optional(
|
|
1460
|
-
Type.String({
|
|
1461
|
-
description:
|
|
1462
|
-
"Only include results published on or after this date (YYYY-MM-DD).",
|
|
1463
|
-
}),
|
|
1464
|
-
),
|
|
1465
|
-
endDate: Type.Optional(
|
|
1466
|
-
Type.String({
|
|
1467
|
-
description:
|
|
1468
|
-
"Only include results published on or before this date (YYYY-MM-DD).",
|
|
1469
|
-
}),
|
|
1470
|
-
),
|
|
1471
|
-
}),
|
|
1472
|
-
async execute(_toolCallId, params) {
|
|
1473
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1474
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1475
|
-
const {
|
|
1476
|
-
query,
|
|
1477
|
-
domain: rawDomain,
|
|
1478
|
-
maxResults,
|
|
1479
|
-
startDate,
|
|
1480
|
-
endDate,
|
|
1481
|
-
} = params as {
|
|
1482
|
-
query: string;
|
|
1483
|
-
domain?: string;
|
|
1484
|
-
maxResults?: number;
|
|
1485
|
-
startDate?: string;
|
|
1486
|
-
endDate?: string;
|
|
1487
|
-
};
|
|
1488
|
-
const domain: WebSearchDomain =
|
|
1489
|
-
rawDomain === undefined || rawDomain === null
|
|
1490
|
-
? "web"
|
|
1491
|
-
: normalizeWebSearchDomain(rawDomain);
|
|
1492
|
-
|
|
1493
|
-
try {
|
|
1494
|
-
const response = await effectiveProvider.searchWeb(query, {
|
|
1495
|
-
domain,
|
|
1496
|
-
max_results: maxResults ?? effectiveCfg.topK,
|
|
1497
|
-
start_date: startDate,
|
|
1498
|
-
end_date: endDate,
|
|
1499
|
-
include_domains: effectiveCfg.webSearchIncludeDomains,
|
|
1500
|
-
exclude_domains: effectiveCfg.webSearchExcludeDomains,
|
|
1501
|
-
user_location:
|
|
1502
|
-
effectiveCfg.webSearchCountry || effectiveCfg.webSearchTimezone
|
|
1503
|
-
? {
|
|
1504
|
-
country: effectiveCfg.webSearchCountry,
|
|
1505
|
-
timezone: effectiveCfg.webSearchTimezone,
|
|
1506
|
-
}
|
|
1507
|
-
: undefined,
|
|
1508
|
-
});
|
|
1509
|
-
|
|
1510
|
-
if (!response.results || response.results.length === 0) {
|
|
1511
|
-
return {
|
|
1512
|
-
content: [
|
|
1513
|
-
{ type: "text", text: "No relevant web results found." },
|
|
1514
|
-
],
|
|
1515
|
-
details: { count: 0, total_results: response.total_results },
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
const context = buildWebSearchContext(response.results);
|
|
1520
|
-
|
|
1521
|
-
return {
|
|
1522
|
-
content: [
|
|
1523
|
-
{
|
|
1524
|
-
type: "text",
|
|
1525
|
-
text: `Found ${response.results.length} web results:\n\n${context}`,
|
|
1526
|
-
},
|
|
1527
|
-
],
|
|
1528
|
-
details: {
|
|
1529
|
-
count: response.results.length,
|
|
1530
|
-
total_results: response.total_results,
|
|
1531
|
-
results: response.results,
|
|
1532
|
-
},
|
|
1533
|
-
};
|
|
1534
|
-
} catch (err) {
|
|
1535
|
-
return {
|
|
1536
|
-
content: [
|
|
1537
|
-
{
|
|
1538
|
-
type: "text",
|
|
1539
|
-
text: `Web search failed: ${String(err)}`,
|
|
1540
|
-
},
|
|
1541
|
-
],
|
|
1542
|
-
details: { error: String(err) },
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
},
|
|
1546
|
-
}),
|
|
1547
|
-
{ optional: true },
|
|
1548
|
-
);
|
|
1549
|
-
|
|
1550
|
-
api.registerTool(
|
|
1551
|
-
(ctx) => ({
|
|
1552
|
-
name: "open_data_search",
|
|
1553
|
-
label: "Open Data Search",
|
|
1554
|
-
description:
|
|
1555
|
-
"Search across open datasets routed to the appropriate proprietary data source based on the dataset:\n- research/academic: arXiv, PubMed, bioRxiv, medRxiv\n- clinical/trials: Clinical trial registries\n- drug/database: ChEMBL, DrugBank, PubChem, etc.\n- financial/markets: Stocks, crypto, forex, funds, commodities\n- company/fundamentals: SEC filings, earnings, balance sheets, etc.\n- economic/data: FRED, BLS, World Bank, etc.\n- patents/ip: USPTO patents",
|
|
1556
|
-
parameters: Type.Object({
|
|
1557
|
-
query: Type.String({
|
|
1558
|
-
description: "The search query to send to the open data endpoint.",
|
|
1559
|
-
}),
|
|
1560
|
-
dataset: Type.Union(
|
|
1561
|
-
[
|
|
1562
|
-
Type.Literal("research/academic"),
|
|
1563
|
-
Type.Literal("clinical/trials"),
|
|
1564
|
-
Type.Literal("drug/database"),
|
|
1565
|
-
Type.Literal("financial/markets"),
|
|
1566
|
-
Type.Literal("company/fundamentals"),
|
|
1567
|
-
Type.Literal("economic/data"),
|
|
1568
|
-
Type.Literal("patents/ip"),
|
|
1569
|
-
],
|
|
1570
|
-
{
|
|
1571
|
-
description:
|
|
1572
|
-
"Dataset category to search. Must be one of the project's enabled categories.",
|
|
1573
|
-
},
|
|
1574
|
-
),
|
|
1575
|
-
maxResults: Type.Optional(
|
|
1576
|
-
Type.Number({
|
|
1577
|
-
description: `Maximum number of results to return (default: ${cfg.topK}). The server enforces a hard cap.`,
|
|
1578
|
-
minimum: 1,
|
|
1579
|
-
}),
|
|
1580
|
-
),
|
|
1581
|
-
startDate: Type.Optional(
|
|
1582
|
-
Type.String({
|
|
1583
|
-
description: "Only include results published on or after this date (YYYY-MM-DD).",
|
|
1584
|
-
}),
|
|
1585
|
-
),
|
|
1586
|
-
endDate: Type.Optional(
|
|
1587
|
-
Type.String({
|
|
1588
|
-
description: "Only include results published on or before this date (YYYY-MM-DD).",
|
|
1589
|
-
}),
|
|
1590
|
-
),
|
|
1591
|
-
}),
|
|
1592
|
-
async execute(_toolCallId, params) {
|
|
1593
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1594
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
1595
|
-
const {
|
|
1596
|
-
query,
|
|
1597
|
-
dataset: rawDataset,
|
|
1598
|
-
maxResults,
|
|
1599
|
-
startDate,
|
|
1600
|
-
endDate,
|
|
1601
|
-
} = params as {
|
|
1602
|
-
query: string;
|
|
1603
|
-
dataset: string;
|
|
1604
|
-
maxResults?: number;
|
|
1605
|
-
startDate?: string;
|
|
1606
|
-
endDate?: string;
|
|
1607
|
-
};
|
|
1608
|
-
|
|
1609
|
-
// Normalize once; use throughout to avoid casing bugs
|
|
1610
|
-
const dataset = normalizeOpenDataCategory(rawDataset);
|
|
1611
|
-
|
|
1612
|
-
if (!dataset) {
|
|
1613
|
-
return {
|
|
1614
|
-
content: [
|
|
1615
|
-
{
|
|
1616
|
-
type: "text",
|
|
1617
|
-
text: `Unsupported dataset: "${rawDataset}". Supported values are: ${OpenDataCategoryValues.join(", ")}`,
|
|
1618
|
-
},
|
|
1619
|
-
],
|
|
1620
|
-
details: { error: "unsupported_dataset", dataset: rawDataset },
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
try {
|
|
1625
|
-
// Validate dataset against project's allowed industries
|
|
1626
|
-
const projectInfo = await effectiveProvider.getProject();
|
|
1627
|
-
if (projectInfo.industries.length > 0) {
|
|
1628
|
-
const allowedIds = projectInfo.industries.map((ind) => ind.id);
|
|
1629
|
-
if (!allowedIds.includes(dataset)) {
|
|
1630
|
-
const allowed = projectInfo.industries
|
|
1631
|
-
.map((ind) => `${ind.id} (${ind.name})`)
|
|
1632
|
-
.join(", ");
|
|
1633
|
-
return {
|
|
1634
|
-
content: [
|
|
1635
|
-
{
|
|
1636
|
-
type: "text",
|
|
1637
|
-
text: `Dataset "${dataset}" is not enabled for this project. Allowed datasets: ${allowed}`,
|
|
1638
|
-
},
|
|
1639
|
-
],
|
|
1640
|
-
details: {
|
|
1641
|
-
error: "dataset_not_allowed",
|
|
1642
|
-
dataset,
|
|
1643
|
-
allowed_datasets: allowedIds,
|
|
1644
|
-
},
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
const response = await effectiveProvider.searchOpenData(query, {
|
|
1650
|
-
dataset,
|
|
1651
|
-
max_results: maxResults ?? effectiveCfg.topK,
|
|
1652
|
-
start_date: startDate,
|
|
1653
|
-
end_date: endDate,
|
|
1654
|
-
});
|
|
1655
|
-
|
|
1656
|
-
if (!response.results || response.results.length === 0) {
|
|
1657
|
-
return {
|
|
1658
|
-
content: [
|
|
1659
|
-
{ type: "text", text: "No relevant open data results found." },
|
|
1660
|
-
],
|
|
1661
|
-
details: { count: 0, total_results: response.total_results },
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
const context = buildOpenDataContext(response.results);
|
|
1666
|
-
|
|
1667
|
-
return {
|
|
1668
|
-
content: [
|
|
1669
|
-
{
|
|
1670
|
-
type: "text",
|
|
1671
|
-
text: `Found ${response.results.length} open data results:\n\n${context}`,
|
|
1672
|
-
},
|
|
1673
|
-
],
|
|
1674
|
-
details: {
|
|
1675
|
-
count: response.results.length,
|
|
1676
|
-
total_results: response.total_results,
|
|
1677
|
-
results: response.results,
|
|
1678
|
-
},
|
|
1679
|
-
};
|
|
1680
|
-
} catch (err) {
|
|
1681
|
-
return {
|
|
1682
|
-
content: [
|
|
1683
|
-
{
|
|
1684
|
-
type: "text",
|
|
1685
|
-
text: `Open data search failed: ${String(err)}`,
|
|
1686
|
-
},
|
|
1687
|
-
],
|
|
1688
|
-
details: { error: String(err) },
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
},
|
|
1692
|
-
}),
|
|
1693
|
-
);
|
|
1694
|
-
|
|
1695
|
-
// ========================================================================
|
|
1696
|
-
// CLI Commands
|
|
1697
|
-
// ========================================================================
|
|
1698
|
-
|
|
1699
|
-
api.registerCli(
|
|
1700
|
-
({ program }) => {
|
|
1701
|
-
const memorylake = program
|
|
1702
|
-
.command("memorylake")
|
|
1703
|
-
.description("MemoryLake memory plugin commands");
|
|
1704
|
-
|
|
1705
|
-
memorylake
|
|
1706
|
-
.command("search")
|
|
1707
|
-
.description("Search memories in MemoryLake")
|
|
1708
|
-
.argument("<query>", "Search query")
|
|
1709
|
-
.option("--limit <n>", "Max results", String(cfg.topK))
|
|
1710
|
-
.action(async (query: string, opts: { limit: string }) => {
|
|
1711
|
-
try {
|
|
1712
|
-
const limit = parseInt(opts.limit, 10);
|
|
1713
|
-
const results = await provider.search(
|
|
1714
|
-
query,
|
|
1715
|
-
buildSearchOptions(cfg, undefined, limit),
|
|
1716
|
-
);
|
|
1717
|
-
|
|
1718
|
-
if (!results.length) {
|
|
1719
|
-
console.log("No memories found.");
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
const output = results.map((r) => ({
|
|
1724
|
-
id: r.id,
|
|
1725
|
-
content: r.content,
|
|
1726
|
-
user_id: r.user_id,
|
|
1727
|
-
created_at: r.created_at,
|
|
1728
|
-
}));
|
|
1729
|
-
console.log(JSON.stringify(output, null, 2));
|
|
1730
|
-
} catch (err) {
|
|
1731
|
-
console.error(`Search failed: ${String(err)}`);
|
|
1732
|
-
}
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
|
-
memorylake
|
|
1736
|
-
.command("upload")
|
|
1737
|
-
.description("Upload files or directories to MemoryLake")
|
|
1738
|
-
.argument("<path>", "File or directory path to upload")
|
|
1739
|
-
.option("--agent <id>", "Agent ID (resolves workspace and per-agent projectId)")
|
|
1740
|
-
.option("--project-id <id>", "Override project ID (takes precedence over --agent)")
|
|
1741
|
-
.action(async (targetPath: string, opts: { agent?: string; projectId?: string }) => {
|
|
1742
|
-
// Resolve effective config: --project-id > agent workspace config > global config
|
|
1743
|
-
let effectiveCfg = cfg;
|
|
1744
|
-
if (opts.agent) {
|
|
1745
|
-
try {
|
|
1746
|
-
const openclawPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
1747
|
-
const openclaw = JSON.parse(fs.readFileSync(openclawPath, "utf-8"));
|
|
1748
|
-
const agents = openclaw?.agents;
|
|
1749
|
-
const agentEntry = agents?.list?.find((a: any) => a.id === opts.agent);
|
|
1750
|
-
const workspace = agentEntry?.workspace || agents?.defaults?.workspace;
|
|
1751
|
-
if (workspace) {
|
|
1752
|
-
effectiveCfg = resolveConfig({ workspaceDir: workspace });
|
|
1753
|
-
} else {
|
|
1754
|
-
console.warn(`Warning: no workspace found for agent "${opts.agent}", using global config.`);
|
|
1755
|
-
}
|
|
1756
|
-
} catch (err) {
|
|
1757
|
-
console.warn(`Warning: failed to resolve agent config: ${String(err)}, using global config.`);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
const effectiveProjectId = opts.projectId || effectiveCfg.projectId;
|
|
1761
|
-
if (!effectiveProjectId) {
|
|
1762
|
-
console.error("No project ID configured. Use --project-id or set up agent/workspace config.");
|
|
1763
|
-
return;
|
|
1764
|
-
}
|
|
1765
|
-
if (!effectiveCfg.host || !effectiveCfg.apiKey) {
|
|
1766
|
-
console.error("Missing host or apiKey in config. Check your MemoryLake configuration.");
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// Lazy import upload.mjs (use uploadAuto to support archives)
|
|
1771
|
-
let uploadFn: UploadFn;
|
|
1772
|
-
try {
|
|
1773
|
-
const uploadModule = await import(
|
|
1774
|
-
/* webpackIgnore: true */
|
|
1775
|
-
new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
|
|
1776
|
-
);
|
|
1777
|
-
uploadFn = uploadModule.uploadAuto;
|
|
1778
|
-
} catch (err) {
|
|
1779
|
-
console.error(`Failed to load upload module: ${String(err)}`);
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
const absPath = path.resolve(targetPath);
|
|
1784
|
-
|
|
1785
|
-
try {
|
|
1786
|
-
await uploadFn({
|
|
1787
|
-
host: effectiveCfg.host,
|
|
1788
|
-
apiKey: effectiveCfg.apiKey,
|
|
1789
|
-
projectId: effectiveProjectId,
|
|
1790
|
-
filePath: absPath,
|
|
1791
|
-
fileName: path.basename(absPath),
|
|
1792
|
-
});
|
|
1793
|
-
} catch (err) {
|
|
1794
|
-
console.error(`Upload failed: ${String(err)}`);
|
|
1795
|
-
}
|
|
1796
|
-
});
|
|
1797
|
-
|
|
1798
|
-
memorylake
|
|
1799
|
-
.command("stats")
|
|
1800
|
-
.description("Show memory statistics from MemoryLake")
|
|
1801
|
-
.action(async () => {
|
|
1802
|
-
try {
|
|
1803
|
-
const memories = await provider.getAll({
|
|
1804
|
-
user_id: cfg.userId,
|
|
1805
|
-
});
|
|
1806
|
-
console.log(`User: ${cfg.userId}`);
|
|
1807
|
-
console.log(
|
|
1808
|
-
`Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`,
|
|
1809
|
-
);
|
|
1810
|
-
console.log(
|
|
1811
|
-
`Auto-recall: ${cfg.autoRecall}, Auto-capture: ${cfg.autoCapture}`,
|
|
1812
|
-
);
|
|
1813
|
-
} catch (err) {
|
|
1814
|
-
console.error(`Stats failed: ${String(err)}`);
|
|
1815
|
-
}
|
|
1816
|
-
});
|
|
1817
|
-
},
|
|
1818
|
-
{ commands: ["memorylake"] },
|
|
1819
|
-
);
|
|
1820
|
-
|
|
1821
|
-
// ========================================================================
|
|
1822
|
-
// Lifecycle Hooks
|
|
1823
|
-
// ========================================================================
|
|
1824
|
-
|
|
1825
|
-
// --- Auto-upload helpers ---
|
|
1826
|
-
const UPLOADED_RECORD_FILE = "uploaded.json";
|
|
1827
|
-
|
|
1828
|
-
type UploadedRecord = Record<string, { mtimeMs: number }>;
|
|
1829
|
-
|
|
1830
|
-
function getUploadedRecord(workspaceDir: string): UploadedRecord {
|
|
1831
|
-
const filePath = path.join(workspaceDir, ".memorylake", UPLOADED_RECORD_FILE);
|
|
1832
|
-
try {
|
|
1833
|
-
if (fs.existsSync(filePath)) {
|
|
1834
|
-
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1835
|
-
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
1836
|
-
}
|
|
1837
|
-
} catch (err) {
|
|
1838
|
-
api.logger.warn(`memorylake-openclaw: failed to read uploaded record: ${String(err)}`);
|
|
1839
|
-
}
|
|
1840
|
-
return {};
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
function saveUploadedRecord(workspaceDir: string, record: UploadedRecord): void {
|
|
1844
|
-
const dirPath = path.join(workspaceDir, ".memorylake");
|
|
1845
|
-
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
1846
|
-
fs.writeFileSync(
|
|
1847
|
-
path.join(dirPath, UPLOADED_RECORD_FILE),
|
|
1848
|
-
JSON.stringify(record, null, 2),
|
|
1849
|
-
);
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
function needsUpload(record: UploadedRecord, filePath: string): fs.Stats | null {
|
|
1853
|
-
if (!fs.existsSync(filePath)) return null;
|
|
1854
|
-
const stat = fs.statSync(filePath);
|
|
1855
|
-
const prev = record[filePath];
|
|
1856
|
-
return (!prev || prev.mtimeMs !== stat.mtimeMs) ? stat : null;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
function extractInboundPaths(prompt: string): string[] {
|
|
1860
|
-
// Path must contain /media/inbound/ (or \media\inbound\)
|
|
1861
|
-
// Filename must end with .<ext>, ext = alphanumeric, 1-6 chars
|
|
1862
|
-
const sep = '[/\\\\]';
|
|
1863
|
-
const regex = new RegExp(
|
|
1864
|
-
`(?:[A-Za-z]:${sep}|/)\\S*?media${sep}inbound${sep}.+?\\.[a-zA-Z0-9]{1,6}(?=[^a-zA-Z0-9]|$)`,
|
|
1865
|
-
"g",
|
|
1866
|
-
);
|
|
1867
|
-
const matches = prompt.match(regex) || [];
|
|
1868
|
-
return [...new Set(matches)];
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
// Auto-upload: upload inbound files to MemoryLake before prompt build
|
|
1872
|
-
if (cfg.autoUpload) {
|
|
1873
|
-
// Lazy-load upload function from upload.mjs
|
|
1874
|
-
let uploadAutoFn: UploadFn | undefined;
|
|
1875
|
-
|
|
1876
|
-
api.on("before_prompt_build", (event, ctx) => {
|
|
1877
|
-
if ((ctx as any)?.trigger !== "user") {
|
|
1878
|
-
api.logger.info(`memorylake-openclaw: auto-upload skipped, trigger=${(ctx as any)?.trigger ?? "undefined"}`);
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
const workspaceDir = (ctx as any)?.workspaceDir;
|
|
1882
|
-
if (!workspaceDir || !event.prompt) return;
|
|
1883
|
-
|
|
1884
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
1885
|
-
const paths = extractInboundPaths(event.prompt);
|
|
1886
|
-
if (paths.length === 0) return;
|
|
1887
|
-
|
|
1888
|
-
const record = getUploadedRecord(workspaceDir);
|
|
1889
|
-
const filesToUpload: { filePath: string; stat: fs.Stats }[] = [];
|
|
1890
|
-
for (const p of paths) {
|
|
1891
|
-
const stat = needsUpload(record, p);
|
|
1892
|
-
if (stat) filesToUpload.push({ filePath: p, stat });
|
|
1893
|
-
}
|
|
1894
|
-
if (filesToUpload.length === 0) return;
|
|
1895
|
-
|
|
1896
|
-
// Fire-and-forget: upload asynchronously without blocking
|
|
1897
|
-
(async () => {
|
|
1898
|
-
// Lazy import upload.mjs
|
|
1899
|
-
if (!uploadAutoFn) {
|
|
1900
|
-
const uploadModule = await import(
|
|
1901
|
-
/* webpackIgnore: true */
|
|
1902
|
-
new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
|
|
1903
|
-
);
|
|
1904
|
-
uploadAutoFn = uploadModule.uploadAuto;
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
for (const { filePath, stat } of filesToUpload) {
|
|
1908
|
-
try {
|
|
1909
|
-
await uploadAutoFn!({
|
|
1910
|
-
host: effectiveCfg.host,
|
|
1911
|
-
apiKey: effectiveCfg.apiKey,
|
|
1912
|
-
projectId: effectiveCfg.projectId,
|
|
1913
|
-
filePath,
|
|
1914
|
-
fileName: path.basename(filePath),
|
|
1915
|
-
});
|
|
1916
|
-
// Save record only after successful upload to avoid race on crash
|
|
1917
|
-
const current = getUploadedRecord(workspaceDir);
|
|
1918
|
-
current[filePath] = { mtimeMs: stat.mtimeMs };
|
|
1919
|
-
saveUploadedRecord(workspaceDir, current);
|
|
1920
|
-
api.logger.info(
|
|
1921
|
-
`memorylake-openclaw: auto-uploaded ${path.basename(filePath)}`,
|
|
1922
|
-
);
|
|
1923
|
-
} catch (err) {
|
|
1924
|
-
api.logger.warn(
|
|
1925
|
-
`memorylake-openclaw: auto-upload failed for ${filePath}: ${String(err)}`,
|
|
1926
|
-
);
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
})().catch((err) => {
|
|
1930
|
-
api.logger.warn(
|
|
1931
|
-
`memorylake-openclaw: auto-upload unexpected error: ${String(err)}`,
|
|
1932
|
-
);
|
|
1933
|
-
});
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
// ------------------------------------------------------------------
|
|
1938
|
-
// LLM Query Rewrite Helpers
|
|
1939
|
-
// ------------------------------------------------------------------
|
|
1940
|
-
|
|
1941
|
-
/**
|
|
1942
|
-
* Summarize recent session messages into a compact text block for the rewrite prompt.
|
|
1943
|
-
* Messages are unknown[] from the hook event — we extract role+content from each.
|
|
1944
|
-
*/
|
|
1945
|
-
function summarizeMessages(messages: unknown[], maxMessages = 10): string {
|
|
1946
|
-
if (!messages || messages.length === 0) return "";
|
|
1947
|
-
const recent = messages.slice(-maxMessages);
|
|
1948
|
-
return recent
|
|
1949
|
-
.map((m: any) => {
|
|
1950
|
-
const role = m?.role ?? "user";
|
|
1951
|
-
const content =
|
|
1952
|
-
typeof m?.content === "string"
|
|
1953
|
-
? m.content
|
|
1954
|
-
: JSON.stringify(m?.content ?? "");
|
|
1955
|
-
return `[${role}]: ${content}`;
|
|
1956
|
-
})
|
|
1957
|
-
.join("\n");
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
// (loadCoreAgentDeps is defined at module scope above)
|
|
1961
|
-
|
|
1962
|
-
/**
|
|
1963
|
-
* Resolve provider/model from config. Returns undefined for both if not found
|
|
1964
|
-
* (openclaw will use its own defaults).
|
|
1965
|
-
*/
|
|
1966
|
-
function resolveProviderModel(): { provider: string | undefined; model: string | undefined } {
|
|
1967
|
-
const modelPrimary = (api.config as any)?.agents?.defaults?.model?.primary as string | undefined;
|
|
1968
|
-
if (modelPrimary) {
|
|
1969
|
-
const slashIdx = modelPrimary.indexOf("/");
|
|
1970
|
-
if (slashIdx >= 0) {
|
|
1971
|
-
return { provider: modelPrimary.slice(0, slashIdx), model: modelPrimary.slice(slashIdx + 1) };
|
|
1972
|
-
}
|
|
1973
|
-
return { provider: undefined, model: modelPrimary };
|
|
1974
|
-
}
|
|
1975
|
-
return { provider: undefined, model: undefined };
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
/**
|
|
1979
|
-
* Rewrite the user's prompt into a search-optimized query using
|
|
1980
|
-
* openclaw's runEmbeddedPiAgent, considering conversation history.
|
|
1981
|
-
*
|
|
1982
|
-
* Priority: api.runtime.agent.runEmbeddedPiAgent → loadCoreAgentDeps()
|
|
1983
|
-
*/
|
|
1984
|
-
async function rewriteQueryForSearch(
|
|
1985
|
-
originalPrompt: string,
|
|
1986
|
-
messages: unknown[],
|
|
1987
|
-
ctx: { workspaceDir?: string },
|
|
1988
|
-
): Promise<string> {
|
|
1989
|
-
if (!ctx.workspaceDir) {
|
|
1990
|
-
api.logger.warn("memorylake-openclaw: no workspaceDir, skipping query rewrite");
|
|
1991
|
-
return originalPrompt;
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
const conversationHistory = summarizeMessages(messages);
|
|
1995
|
-
const systemPrompt =
|
|
1996
|
-
"You are a search query optimizer. Extract the key search intent and produce a concise, search-optimized query. Output ONLY the rewritten query, nothing else. Preserve important entities, names, dates, and technical terms.";
|
|
1997
|
-
const userContent = conversationHistory
|
|
1998
|
-
? `Conversation history:\n${conversationHistory}\n\nUser's latest message:\n${originalPrompt}`
|
|
1999
|
-
: originalPrompt;
|
|
2000
|
-
const fullPrompt = `${systemPrompt}\n\n${userContent}`;
|
|
2001
|
-
|
|
2002
|
-
const { provider, model } = resolveProviderModel();
|
|
2003
|
-
api.logger.info(`memorylake-openclaw: rewriting query via runEmbeddedPiAgent (provider=${provider}, model=${model})`);
|
|
2004
|
-
|
|
2005
|
-
let tempSessionFile: string | null = null;
|
|
2006
|
-
try {
|
|
2007
|
-
const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "memorylake-rewrite-"));
|
|
2008
|
-
tempSessionFile = path.join(tempDir, "session.jsonl");
|
|
2009
|
-
|
|
2010
|
-
const nowMs = Date.now();
|
|
2011
|
-
const callParams = {
|
|
2012
|
-
sessionId: `memorylake-rewrite-${nowMs}`,
|
|
2013
|
-
sessionKey: `temp:memorylake-rewrite`,
|
|
2014
|
-
sessionFile: tempSessionFile,
|
|
2015
|
-
workspaceDir: ctx.workspaceDir,
|
|
2016
|
-
config: api.config,
|
|
2017
|
-
prompt: fullPrompt,
|
|
2018
|
-
provider,
|
|
2019
|
-
model,
|
|
2020
|
-
disableTools: true,
|
|
2021
|
-
timeoutMs: 15_000,
|
|
2022
|
-
runId: `memorylake-rewrite-${nowMs}`,
|
|
2023
|
-
lane: `memorylake-rewrite`,
|
|
2024
|
-
trigger: "memory",
|
|
2025
|
-
};
|
|
2026
|
-
|
|
2027
|
-
// Priority 1: try api.runtime.agent.runEmbeddedPiAgent
|
|
2028
|
-
let runEmbeddedPiAgent: ((p: typeof callParams) => Promise<any>) | undefined =
|
|
2029
|
-
(api.runtime as any)?.agent?.runEmbeddedPiAgent;
|
|
2030
|
-
|
|
2031
|
-
if (typeof runEmbeddedPiAgent !== "function") {
|
|
2032
|
-
api.logger.info("memorylake-openclaw: api.runtime.agent.runEmbeddedPiAgent not available, using loadCoreAgentDeps fallback");
|
|
2033
|
-
const deps = await loadCoreAgentDeps();
|
|
2034
|
-
runEmbeddedPiAgent = deps.runEmbeddedPiAgent;
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
const result = await runEmbeddedPiAgent(callParams);
|
|
2038
|
-
|
|
2039
|
-
const rewritten = result?.payloads?.[0]?.text?.trim();
|
|
2040
|
-
if (rewritten && rewritten.length > 0) {
|
|
2041
|
-
api.logger.info(`memorylake-openclaw: rewritten query: "${rewritten}"`);
|
|
2042
|
-
return rewritten;
|
|
2043
|
-
}
|
|
2044
|
-
api.logger.warn("memorylake-openclaw: rewrite returned empty, using original");
|
|
2045
|
-
} catch (err) {
|
|
2046
|
-
api.logger.warn(`memorylake-openclaw: query rewrite failed, using original: ${String(err)}`);
|
|
2047
|
-
} finally {
|
|
2048
|
-
if (tempSessionFile) {
|
|
2049
|
-
try {
|
|
2050
|
-
await fsPromises.rm(path.dirname(tempSessionFile), { recursive: true, force: true });
|
|
2051
|
-
} catch (cleanupErr) {
|
|
2052
|
-
api.logger.warn(`memorylake-openclaw: temp session cleanup failed: ${String(cleanupErr)}`);
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
return originalPrompt;
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
// ------------------------------------------------------------------
|
|
2060
|
-
// Auto-recall: inject system-level memory instructions and open data
|
|
2061
|
-
// categories before prompt build. Memory content is NOT pre-fetched;
|
|
2062
|
-
// the model is instructed to call memory_search itself.
|
|
2063
|
-
// ------------------------------------------------------------------
|
|
2064
|
-
if (cfg.autoRecall) {
|
|
2065
|
-
api.on("before_prompt_build", async (event, ctx) => {
|
|
2066
|
-
if ((ctx as any)?.trigger !== "user") {
|
|
2067
|
-
api.logger.info(`memorylake-openclaw: auto-recall skipped, trigger=${(ctx as any)?.trigger ?? "undefined"}`);
|
|
2068
|
-
return;
|
|
2069
|
-
}
|
|
2070
|
-
if (!event.prompt) return;
|
|
2071
|
-
|
|
2072
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
2073
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
2074
|
-
|
|
2075
|
-
const sessionId = (ctx as any)?.sessionId ?? undefined;
|
|
2076
|
-
|
|
2077
|
-
// Fetch industries once per session, then cache
|
|
2078
|
-
let industries: OpenDataIndustry[] | undefined;
|
|
2079
|
-
if (sessionId && sessionIndustriesCache.has(sessionId)) {
|
|
2080
|
-
industries = sessionIndustriesCache.get(sessionId);
|
|
2081
|
-
} else {
|
|
2082
|
-
try {
|
|
2083
|
-
const projectInfo = await effectiveProvider.getProject();
|
|
2084
|
-
industries = projectInfo.industries;
|
|
2085
|
-
if (sessionId) {
|
|
2086
|
-
sessionIndustriesCache.set(sessionId, industries);
|
|
2087
|
-
}
|
|
2088
|
-
} catch (err) {
|
|
2089
|
-
api.logger.warn(`memorylake-openclaw: project info fetch failed: ${String(err)}`);
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
const result: { prependSystemContext?: string; appendSystemContext?: string } = {};
|
|
2094
|
-
|
|
2095
|
-
const memoryDirectiveParts: string[] = [
|
|
2096
|
-
`<memory-instructions>`,
|
|
2097
|
-
`You have access to the user's long-term memory AND documents via MemoryLake (memory_search tool).`,
|
|
2098
|
-
`The memory_search tool searches both memories and uploaded documents in a single call.`,
|
|
2099
|
-
``,
|
|
2100
|
-
`MANDATORY PROCEDURE for EVERY user message:`,
|
|
2101
|
-
`1. Read the user's message.`,
|
|
2102
|
-
`2. Call memory_search with a query derived from the message — this step is NOT optional.`,
|
|
2103
|
-
`3. Use the search results (memories + documents) to personalize your response.`,
|
|
2104
|
-
`4. Respond to the user.`,
|
|
2105
|
-
``,
|
|
2106
|
-
`This applies to ALL messages — greetings, recommendations, advice, casual chat, tasks, questions.`,
|
|
2107
|
-
`The user's preferences, history, name, hobbies, context, and documents are stored in MemoryLake.`,
|
|
2108
|
-
`A response without memory_search is INCORRECT because it ignores personalized context.`,
|
|
2109
|
-
`</memory-instructions>`,
|
|
2110
|
-
];
|
|
2111
|
-
result.prependSystemContext = memoryDirectiveParts.join("\n");
|
|
2112
|
-
|
|
2113
|
-
const appendParts: string[] = [];
|
|
2114
|
-
if (industries && industries.length > 0) {
|
|
2115
|
-
const categoryList = industries
|
|
2116
|
-
.map((ind) => `- ${ind.id}: ${ind.name}${ind.description ? ` — ${ind.description}` : ""}`)
|
|
2117
|
-
.join("\n");
|
|
2118
|
-
appendParts.push(
|
|
2119
|
-
`<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>`,
|
|
2120
|
-
);
|
|
2121
|
-
api.logger.info(
|
|
2122
|
-
`memorylake-openclaw: injecting ${industries.length} open data categories into system context`,
|
|
2123
|
-
);
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
if (appendParts.length > 0) {
|
|
2127
|
-
result.appendSystemContext = appendParts.join("\n\n");
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
return result;
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
// Auto-capture: store conversation context after agent ends
|
|
2135
|
-
if (cfg.autoCapture) {
|
|
2136
|
-
api.on("agent_end", async (event, ctx) => {
|
|
2137
|
-
if ((ctx as any)?.trigger !== "user") {
|
|
2138
|
-
api.logger.info(`memorylake-openclaw: auto-capture skipped, trigger=${(ctx as any)?.trigger ?? "undefined"}`);
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
if (!event.success || !event.messages || event.messages.length === 0) {
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
// Resolve per-workspace config override
|
|
2146
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
2147
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
2148
|
-
|
|
2149
|
-
// Track session ID
|
|
2150
|
-
const sessionId = (ctx as any)?.sessionId ?? undefined;
|
|
2151
|
-
|
|
2152
|
-
try {
|
|
2153
|
-
// Extract messages, limiting to last 10
|
|
2154
|
-
const recentMessages = event.messages.slice(-10);
|
|
2155
|
-
const formattedMessages: Array<{
|
|
2156
|
-
role: string;
|
|
2157
|
-
content: string;
|
|
2158
|
-
}> = [];
|
|
2159
|
-
|
|
2160
|
-
for (const msg of recentMessages) {
|
|
2161
|
-
if (!msg || typeof msg !== "object") continue;
|
|
2162
|
-
const msgObj = msg as Record<string, unknown>;
|
|
2163
|
-
|
|
2164
|
-
const role = msgObj.role;
|
|
2165
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
2166
|
-
|
|
2167
|
-
let textContent = "";
|
|
2168
|
-
const content = msgObj.content;
|
|
2169
|
-
|
|
2170
|
-
if (typeof content === "string") {
|
|
2171
|
-
textContent = content;
|
|
2172
|
-
} else if (Array.isArray(content)) {
|
|
2173
|
-
for (const block of content) {
|
|
2174
|
-
if (
|
|
2175
|
-
block &&
|
|
2176
|
-
typeof block === "object" &&
|
|
2177
|
-
"text" in block &&
|
|
2178
|
-
typeof (block as Record<string, unknown>).text === "string"
|
|
2179
|
-
) {
|
|
2180
|
-
textContent +=
|
|
2181
|
-
(textContent ? "\n" : "") +
|
|
2182
|
-
((block as Record<string, unknown>).text as string);
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
if (!textContent) continue;
|
|
2188
|
-
// Strip injected context, keep the actual user text
|
|
2189
|
-
if (textContent.includes("<relevant-memories>")) {
|
|
2190
|
-
textContent = textContent.replace(/<relevant-memories>[\s\S]*?<\/relevant-memories>\s*/g, "").trim();
|
|
2191
|
-
}
|
|
2192
|
-
if (textContent.includes("<memory-conflicts>")) {
|
|
2193
|
-
textContent = textContent.replace(/<memory-conflicts>[\s\S]*?<\/memory-conflicts>\s*/g, "").trim();
|
|
2194
|
-
}
|
|
2195
|
-
if (textContent.includes("<relevant-documents>")) {
|
|
2196
|
-
textContent = textContent.replace(/<relevant-documents>[\s\S]*?<\/relevant-documents>\s*/g, "").trim();
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
if (!textContent) continue;
|
|
2200
|
-
|
|
2201
|
-
formattedMessages.push({
|
|
2202
|
-
role: role as string,
|
|
2203
|
-
content: textContent,
|
|
2204
|
-
});
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
if (formattedMessages.length === 0) return;
|
|
2208
|
-
|
|
2209
|
-
const addOpts = buildAddOptions(effectiveCfg, undefined, sessionId);
|
|
2210
|
-
const result = await effectiveProvider.add(
|
|
2211
|
-
formattedMessages,
|
|
2212
|
-
addOpts,
|
|
2213
|
-
);
|
|
2214
|
-
|
|
2215
|
-
const capturedCount = result.results?.length ?? 0;
|
|
2216
|
-
if (capturedCount > 0) {
|
|
2217
|
-
api.logger.info(
|
|
2218
|
-
`memorylake-openclaw: auto-captured ${capturedCount} memories`,
|
|
2219
|
-
);
|
|
2220
|
-
}
|
|
2221
|
-
} catch (err) {
|
|
2222
|
-
api.logger.warn(`memorylake-openclaw: capture failed: ${String(err)}`);
|
|
2223
|
-
}
|
|
2224
|
-
});
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
// ========================================================================
|
|
2228
|
-
// Service
|
|
2229
|
-
// ========================================================================
|
|
43
|
+
registerMemoryTools(pctx, cfg);
|
|
44
|
+
registerDocumentTools(pctx);
|
|
45
|
+
registerSearchTools(pctx, cfg);
|
|
46
|
+
registerCli(pctx, cfg);
|
|
47
|
+
if (cfg.autoUpload) registerAutoUpload(pctx);
|
|
48
|
+
if (cfg.autoRecall) registerAutoRecall(pctx);
|
|
49
|
+
if (cfg.autoCapture) registerAutoCapture(pctx);
|
|
2230
50
|
|
|
2231
51
|
api.registerService({
|
|
2232
52
|
id: "memorylake-openclaw",
|