openclaw-mem 1.0.4 → 1.3.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/HOOK.md +125 -0
- package/LICENSE +1 -1
- package/MCP.json +11 -0
- package/README.md +158 -167
- package/backfill-embeddings.js +79 -0
- package/context-builder.js +703 -0
- package/database.js +625 -0
- package/debug-logger.js +280 -0
- package/extractor.js +268 -0
- package/gateway-llm.js +250 -0
- package/handler.js +941 -0
- package/mcp-http-api.js +424 -0
- package/mcp-server.js +605 -0
- package/mem-get.sh +24 -0
- package/mem-search.sh +17 -0
- package/monitor.js +112 -0
- package/package.json +58 -30
- package/realtime-monitor.js +371 -0
- package/session-watcher.js +192 -0
- package/setup.js +114 -0
- package/sync-recent.js +63 -0
- package/README_CN.md +0 -201
- package/bin/openclaw-mem.js +0 -117
- package/docs/locales/README_AR.md +0 -35
- package/docs/locales/README_DE.md +0 -35
- package/docs/locales/README_ES.md +0 -35
- package/docs/locales/README_FR.md +0 -35
- package/docs/locales/README_HE.md +0 -35
- package/docs/locales/README_HI.md +0 -35
- package/docs/locales/README_ID.md +0 -35
- package/docs/locales/README_IT.md +0 -35
- package/docs/locales/README_JA.md +0 -57
- package/docs/locales/README_KO.md +0 -35
- package/docs/locales/README_NL.md +0 -35
- package/docs/locales/README_PL.md +0 -35
- package/docs/locales/README_PT.md +0 -35
- package/docs/locales/README_RU.md +0 -35
- package/docs/locales/README_TH.md +0 -35
- package/docs/locales/README_TR.md +0 -35
- package/docs/locales/README_UK.md +0 -35
- package/docs/locales/README_VI.md +0 -35
- package/docs/logo.svg +0 -32
- package/lib/context-builder.js +0 -415
- package/lib/database.js +0 -309
- package/lib/handler.js +0 -494
- package/scripts/commands.js +0 -141
- package/scripts/init.js +0 -248
package/gateway-llm.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek LLM helper
|
|
3
|
+
* Calls the DeepSeek OpenAI-compatible endpoint to summarize sessions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SUMMARY_SESSION_PREFIX = 'mem-summary:';
|
|
7
|
+
const DEFAULT_DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
|
|
8
|
+
const DEFAULT_DEEPSEEK_MODEL = 'deepseek-chat';
|
|
9
|
+
|
|
10
|
+
function getDeepSeekBaseUrl() {
|
|
11
|
+
return process.env.DEEPSEEK_BASE_URL
|
|
12
|
+
|| DEFAULT_DEEPSEEK_BASE_URL;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getDeepSeekApiKey() {
|
|
16
|
+
return process.env.DEEPSEEK_API_KEY || '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getDeepSeekModel() {
|
|
20
|
+
return process.env.DEEPSEEK_MODEL
|
|
21
|
+
|| DEFAULT_DEEPSEEK_MODEL;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function truncateText(text, maxChars) {
|
|
25
|
+
if (!text) return '';
|
|
26
|
+
if (text.length <= maxChars) return text;
|
|
27
|
+
return text.slice(0, maxChars) + '…';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatTranscript(messages, maxChars = 12000) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (const m of messages) {
|
|
33
|
+
const role = (m.role || 'unknown').toUpperCase();
|
|
34
|
+
const content = String(m.content || '').replace(/\s+/g, ' ').trim();
|
|
35
|
+
if (!content) continue;
|
|
36
|
+
lines.push(`${role}: ${content}`);
|
|
37
|
+
}
|
|
38
|
+
return truncateText(lines.join('\n'), maxChars);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseSummaryJson(text) {
|
|
42
|
+
if (!text) return null;
|
|
43
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
try {
|
|
46
|
+
const obj = JSON.parse(match[0]);
|
|
47
|
+
return obj && typeof obj === 'object' ? obj : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeSummaryFields(obj) {
|
|
54
|
+
if (!obj) return null;
|
|
55
|
+
const pick = (key) => {
|
|
56
|
+
const val = obj[key];
|
|
57
|
+
if (typeof val === 'string') return val.trim();
|
|
58
|
+
if (val == null) return '';
|
|
59
|
+
return String(val).trim();
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
request: pick('request'),
|
|
63
|
+
investigated: pick('investigated'),
|
|
64
|
+
learned: pick('learned'),
|
|
65
|
+
completed: pick('completed'),
|
|
66
|
+
next_steps: pick('next_steps')
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function callGatewayChat(messages, options = {}) {
|
|
71
|
+
const {
|
|
72
|
+
sessionKey = 'unknown',
|
|
73
|
+
temperature = 0.2,
|
|
74
|
+
max_tokens = 300,
|
|
75
|
+
model
|
|
76
|
+
} = options;
|
|
77
|
+
const apiKey = getDeepSeekApiKey();
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
console.log('[openclaw-mem] No DEEPSEEK_API_KEY found');
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const baseUrl = getDeepSeekBaseUrl();
|
|
83
|
+
const resolvedModel = model || getDeepSeekModel();
|
|
84
|
+
const url = `${baseUrl}/chat/completions`;
|
|
85
|
+
const payload = {
|
|
86
|
+
model: resolvedModel,
|
|
87
|
+
stream: false,
|
|
88
|
+
temperature,
|
|
89
|
+
max_tokens,
|
|
90
|
+
messages
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const headers = {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
'Authorization': `Bearer ${apiKey}`
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
console.log('[openclaw-mem] Calling DeepSeek API...');
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers,
|
|
103
|
+
body: JSON.stringify(payload)
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const errText = await res.text();
|
|
107
|
+
console.error('[openclaw-mem] DeepSeek API error:', res.status, errText);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const json = await res.json();
|
|
111
|
+
const content = json?.choices?.[0]?.message?.content || '';
|
|
112
|
+
console.log('[openclaw-mem] DeepSeek response received');
|
|
113
|
+
return content;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('[openclaw-mem] DeepSeek fetch error:', err.message);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function summarizeSession(messages, options = {}) {
|
|
121
|
+
const { sessionKey = 'unknown' } = options;
|
|
122
|
+
const transcript = formatTranscript(messages, 12000);
|
|
123
|
+
if (!transcript) return null;
|
|
124
|
+
|
|
125
|
+
const buildPrompts = (strict = false) => {
|
|
126
|
+
const systemPrompt = `You are a session summarizer for an AI agent memory system. Your summaries help the agent recall past work in future sessions.
|
|
127
|
+
|
|
128
|
+
INSTRUCTIONS:
|
|
129
|
+
- Focus on OUTCOMES and DELIVERABLES, not conversational flow
|
|
130
|
+
- Use action verbs: implemented, fixed, configured, discovered, decided, explored
|
|
131
|
+
- Be specific: include file names, tool names, error messages, key decisions
|
|
132
|
+
- Write in the language the user used (Chinese if they spoke Chinese, English if English)
|
|
133
|
+
|
|
134
|
+
OUTPUT FORMAT: Return ONLY a valid JSON object with these fields:
|
|
135
|
+
{
|
|
136
|
+
"request": "What the user wanted to accomplish (1 sentence, specific)",
|
|
137
|
+
"investigated": "What was explored or researched to fulfill the request",
|
|
138
|
+
"learned": "Key technical insights, discoveries, or new understanding gained",
|
|
139
|
+
"completed": "Concrete deliverables: what was built, fixed, configured, or decided",
|
|
140
|
+
"next_steps": "Unfinished work or logical follow-up actions (null if fully completed)"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
QUALITY GUIDELINES:
|
|
144
|
+
- "request" should capture the real goal, not just "user asked a question"
|
|
145
|
+
- "investigated" should list specific files read, APIs explored, architectures examined
|
|
146
|
+
- "learned" should contain reusable knowledge (not "learned how to do X" but the actual insight)
|
|
147
|
+
- "completed" should be a concrete outcome someone can verify
|
|
148
|
+
- "next_steps" should be actionable, not vague
|
|
149
|
+
|
|
150
|
+
${strict ? 'CRITICAL: Output ONLY the JSON object. No markdown, no explanation, no code fences.' : ''}`;
|
|
151
|
+
|
|
152
|
+
const userPrompt = 'Session transcript:\n' + transcript + '\n\nJSON:';
|
|
153
|
+
return [
|
|
154
|
+
{ role: 'system', content: systemPrompt },
|
|
155
|
+
{ role: 'user', content: userPrompt }
|
|
156
|
+
];
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// First attempt
|
|
160
|
+
let content = await callGatewayChat(buildPrompts(false), { sessionKey, temperature: 0.2, max_tokens: 600 });
|
|
161
|
+
let parsed = parseSummaryJson(content || '');
|
|
162
|
+
if (parsed) return normalizeSummaryFields(parsed);
|
|
163
|
+
|
|
164
|
+
// Retry once with stricter instruction
|
|
165
|
+
content = await callGatewayChat(buildPrompts(true), { sessionKey, temperature: 0.1, max_tokens: 600 });
|
|
166
|
+
parsed = parseSummaryJson(content || '');
|
|
167
|
+
if (parsed) return normalizeSummaryFields(parsed);
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============ Local Embedding Model (Qwen3-Embedding-0.6B) ============
|
|
173
|
+
|
|
174
|
+
const EMBEDDING_MODEL = 'Xenova/multilingual-e5-small';
|
|
175
|
+
const EMBEDDING_DIMS = 384;
|
|
176
|
+
const EMBEDDING_PREFIX = 'query: ';
|
|
177
|
+
|
|
178
|
+
// Singleton: lazily initialized embedding pipeline
|
|
179
|
+
let _extractorPromise = null;
|
|
180
|
+
|
|
181
|
+
function getExtractor() {
|
|
182
|
+
if (!_extractorPromise) {
|
|
183
|
+
_extractorPromise = (async () => {
|
|
184
|
+
try {
|
|
185
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
186
|
+
console.log('[openclaw-mem] Loading embedding model (first run downloads ~110MB)...');
|
|
187
|
+
const extractor = await pipeline('feature-extraction', EMBEDDING_MODEL);
|
|
188
|
+
console.log('[openclaw-mem] Embedding model loaded');
|
|
189
|
+
return extractor;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error('[openclaw-mem] Failed to load embedding model:', err.message);
|
|
192
|
+
_extractorPromise = null; // Allow retry
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
}
|
|
197
|
+
return _extractorPromise;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generate embedding vector for text using local Qwen3-Embedding-0.6B model.
|
|
202
|
+
* Returns Float32Array of 1024 dimensions, or null on failure.
|
|
203
|
+
*/
|
|
204
|
+
export async function callGatewayEmbeddings(text) {
|
|
205
|
+
try {
|
|
206
|
+
const extractor = await getExtractor();
|
|
207
|
+
if (!extractor) return null;
|
|
208
|
+
|
|
209
|
+
const input = EMBEDDING_PREFIX + text;
|
|
210
|
+
const output = await extractor(input, {
|
|
211
|
+
pooling: 'mean',
|
|
212
|
+
normalize: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return new Float32Array(output.data);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('[openclaw-mem] Embedding generation error:', err.message);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Generate embeddings for multiple texts sequentially.
|
|
224
|
+
* Returns array of Float32Array, or null entries on failure.
|
|
225
|
+
*/
|
|
226
|
+
export async function batchEmbeddings(texts) {
|
|
227
|
+
const extractor = await getExtractor();
|
|
228
|
+
if (!extractor) return texts.map(() => null);
|
|
229
|
+
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const text of texts) {
|
|
232
|
+
try {
|
|
233
|
+
const input = EMBEDDING_PREFIX + text;
|
|
234
|
+
const output = await extractor(input, {
|
|
235
|
+
pooling: 'mean',
|
|
236
|
+
normalize: true,
|
|
237
|
+
});
|
|
238
|
+
results.push(new Float32Array(output.data));
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error('[openclaw-mem] Batch embedding error:', err.message);
|
|
241
|
+
results.push(null);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { EMBEDDING_DIMS };
|
|
248
|
+
|
|
249
|
+
export const INTERNAL_SUMMARY_PREFIX = SUMMARY_SESSION_PREFIX;
|
|
250
|
+
export { callGatewayChat };
|