lynkr 2.0.0 → 3.0.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 +226 -15
- package/docs/index.md +230 -11
- package/install.sh +260 -0
- package/package.json +4 -3
- package/src/clients/databricks.js +158 -0
- package/src/clients/routing.js +13 -1
- package/src/config/index.js +68 -1
- package/src/db/index.js +118 -0
- package/src/memory/extractor.js +350 -0
- package/src/memory/index.js +55 -0
- package/src/memory/retriever.js +266 -0
- package/src/memory/search.js +239 -0
- package/src/memory/store.js +411 -0
- package/src/memory/surprise.js +306 -0
- package/src/memory/tools.js +348 -0
- package/src/orchestrator/index.js +170 -0
- package/test/llamacpp-integration.test.js +686 -0
- package/test/memory/extractor.test.js +360 -0
- package/test/memory/retriever.test.js +583 -0
- package/test/memory/search.test.js +389 -0
- package/test/memory/store.test.js +312 -0
- package/test/memory/surprise.test.js +300 -0
- package/test/memory-performance.test.js +472 -0
- package/test/openai-integration.test.js +681 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
const store = require("./store");
|
|
2
|
+
const surprise = require("./surprise");
|
|
3
|
+
const config = require("../config");
|
|
4
|
+
const logger = require("../logger");
|
|
5
|
+
|
|
6
|
+
// Extraction patterns for different memory types
|
|
7
|
+
const EXTRACTION_PATTERNS = {
|
|
8
|
+
preference: [
|
|
9
|
+
/(?:I|user|they|you)\s+(?:prefer|like|want|need|always|never|usually|typically)\s+([^.!?\n]{10,100})/gi,
|
|
10
|
+
/(?:my|your|their|the user's)\s+(?:preference|choice|favorite)\s+(?:is|for|would be)\s+([^.!?\n]{10,100})/gi,
|
|
11
|
+
/(?:should|must|need to)\s+(?:use|implement|do|follow)\s+([^.!?\n]{10,100})/gi,
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
decision: [
|
|
15
|
+
/(?:decided|choosing|going with|selected|picked|opted for)\s+([^.!?\n]{10,100})/gi,
|
|
16
|
+
/(?:the|our|my)\s+(?:approach|strategy|plan|solution)\s+(?:is|will be|should be)\s+([^.!?\n]{10,100})/gi,
|
|
17
|
+
/(?:let's|we'll|I'll)\s+(?:use|implement|go with|choose)\s+([^.!?\n]{10,100})/gi,
|
|
18
|
+
/(?:agreed|confirmed)\s+(?:to|that|on)\s+([^.!?\n]{10,100})/gi,
|
|
19
|
+
],
|
|
20
|
+
|
|
21
|
+
fact: [
|
|
22
|
+
/(?:this|the)\s+(?:project|codebase|application|system)\s+(?:uses|is|has|implements|requires)\s+([^.!?\n]{10,150})/gi,
|
|
23
|
+
/(?:important|note|remember|keep in mind):\s*([^.!?\n]{10,150})/gi,
|
|
24
|
+
/(?:the|this)\s+(?:file|function|class|module|component)\s+(?:is|handles|manages|does)\s+([^.!?\n]{10,150})/gi,
|
|
25
|
+
/(?:currently|now|at the moment)\s+(?:using|implementing|running)\s+([^.!?\n]{10,100})/gi,
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
entity: [
|
|
29
|
+
/(?:file|function|class|module|component|package|library)\s+['"`]?([A-Za-z0-9_./\-]+)['"`]?/gi,
|
|
30
|
+
/in\s+['"`]([A-Za-z0-9_./\-]+\.(?:js|ts|py|java|go|rs|cpp|c|h))['"`]/gi,
|
|
31
|
+
/`([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)`/g, // Code references
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
relationship: [
|
|
35
|
+
/([A-Za-z0-9_]+)\s+(?:depends on|imports|uses|extends|implements|inherits from)\s+([A-Za-z0-9_]+)/gi,
|
|
36
|
+
/([A-Za-z0-9_./\-]+)\s+(?:calls|invokes|references)\s+([A-Za-z0-9_./\-]+)/gi,
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract memories from assistant response and conversation context
|
|
42
|
+
*/
|
|
43
|
+
async function extractMemories(assistantResponse, conversationMessages, context = {}) {
|
|
44
|
+
if (!config.memory?.extraction?.enabled) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { sessionId = null } = context;
|
|
49
|
+
const memories = [];
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Extract assistant message content
|
|
53
|
+
const assistantContent = extractContent(assistantResponse);
|
|
54
|
+
if (!assistantContent) return [];
|
|
55
|
+
|
|
56
|
+
// Get last user message for context
|
|
57
|
+
const lastUserMessage = conversationMessages
|
|
58
|
+
?.filter(m => m.role === 'user')
|
|
59
|
+
?.pop();
|
|
60
|
+
const userContent = lastUserMessage ? extractContent(lastUserMessage) : '';
|
|
61
|
+
|
|
62
|
+
// Extract different types of memories
|
|
63
|
+
const preferences = extractByType(assistantContent, 'preference');
|
|
64
|
+
const decisions = extractByType(assistantContent, 'decision');
|
|
65
|
+
const facts = extractByType(assistantContent, 'fact');
|
|
66
|
+
const entities = extractEntities(assistantContent);
|
|
67
|
+
const relationships = extractRelationships(assistantContent);
|
|
68
|
+
|
|
69
|
+
// Create memory objects with surprise scores
|
|
70
|
+
for (const content of preferences) {
|
|
71
|
+
const memory = await createMemoryWithSurprise({
|
|
72
|
+
content,
|
|
73
|
+
type: 'preference',
|
|
74
|
+
category: 'user',
|
|
75
|
+
sessionId,
|
|
76
|
+
userContent,
|
|
77
|
+
});
|
|
78
|
+
if (memory) memories.push(memory);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const content of decisions) {
|
|
82
|
+
const memory = await createMemoryWithSurprise({
|
|
83
|
+
content,
|
|
84
|
+
type: 'decision',
|
|
85
|
+
category: 'project',
|
|
86
|
+
sessionId,
|
|
87
|
+
userContent,
|
|
88
|
+
});
|
|
89
|
+
if (memory) memories.push(memory);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const content of facts) {
|
|
93
|
+
const memory = await createMemoryWithSurprise({
|
|
94
|
+
content,
|
|
95
|
+
type: 'fact',
|
|
96
|
+
category: classifyCategory(content),
|
|
97
|
+
sessionId,
|
|
98
|
+
userContent,
|
|
99
|
+
});
|
|
100
|
+
if (memory) memories.push(memory);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const entityName of entities) {
|
|
104
|
+
// Track entity
|
|
105
|
+
store.trackEntity('code', entityName, { source: 'extraction' });
|
|
106
|
+
|
|
107
|
+
const memory = await createMemoryWithSurprise({
|
|
108
|
+
content: `Entity: ${entityName}`,
|
|
109
|
+
type: 'entity',
|
|
110
|
+
category: 'code',
|
|
111
|
+
sessionId,
|
|
112
|
+
userContent,
|
|
113
|
+
metadata: { entityName },
|
|
114
|
+
});
|
|
115
|
+
if (memory) memories.push(memory);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const { from, to, relationship } of relationships) {
|
|
119
|
+
const memory = await createMemoryWithSurprise({
|
|
120
|
+
content: `${from} ${relationship} ${to}`,
|
|
121
|
+
type: 'relationship',
|
|
122
|
+
category: 'code',
|
|
123
|
+
sessionId,
|
|
124
|
+
userContent,
|
|
125
|
+
metadata: { from, to, relationship },
|
|
126
|
+
});
|
|
127
|
+
if (memory) memories.push(memory);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
logger.debug({
|
|
131
|
+
sessionId,
|
|
132
|
+
memoriesExtracted: memories.length,
|
|
133
|
+
types: {
|
|
134
|
+
preference: preferences.length,
|
|
135
|
+
decision: decisions.length,
|
|
136
|
+
fact: facts.length,
|
|
137
|
+
entity: entities.length,
|
|
138
|
+
relationship: relationships.length,
|
|
139
|
+
},
|
|
140
|
+
}, 'Memory extraction completed');
|
|
141
|
+
|
|
142
|
+
return memories;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
logger.error({ err, sessionId }, 'Memory extraction failed');
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract content from message (handle different formats)
|
|
151
|
+
*/
|
|
152
|
+
function extractContent(message) {
|
|
153
|
+
if (!message) return '';
|
|
154
|
+
|
|
155
|
+
// Handle different message formats
|
|
156
|
+
if (typeof message === 'string') return message;
|
|
157
|
+
|
|
158
|
+
if (message.content) {
|
|
159
|
+
if (typeof message.content === 'string') return message.content;
|
|
160
|
+
|
|
161
|
+
// Handle array of content blocks
|
|
162
|
+
if (Array.isArray(message.content)) {
|
|
163
|
+
return message.content
|
|
164
|
+
.filter(block => block?.type === 'text' || typeof block === 'string')
|
|
165
|
+
.map(block => typeof block === 'string' ? block : block.text)
|
|
166
|
+
.join('\n');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle choices array (from model responses)
|
|
171
|
+
if (message.choices && Array.isArray(message.choices)) {
|
|
172
|
+
const choice = message.choices[0];
|
|
173
|
+
if (choice?.message?.content) {
|
|
174
|
+
return extractContent(choice.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract memories by type using patterns
|
|
183
|
+
*/
|
|
184
|
+
function extractByType(text, type) {
|
|
185
|
+
const patterns = EXTRACTION_PATTERNS[type] || [];
|
|
186
|
+
const matches = new Set();
|
|
187
|
+
|
|
188
|
+
for (const pattern of patterns) {
|
|
189
|
+
let match;
|
|
190
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
191
|
+
const captured = match[1]?.trim();
|
|
192
|
+
if (captured && captured.length >= 10 && captured.length <= 200) {
|
|
193
|
+
matches.add(captured);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return Array.from(matches);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Extract entity references
|
|
203
|
+
*/
|
|
204
|
+
function extractEntities(text) {
|
|
205
|
+
const entities = new Set();
|
|
206
|
+
const patterns = EXTRACTION_PATTERNS.entity;
|
|
207
|
+
|
|
208
|
+
for (const pattern of patterns) {
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
211
|
+
const entityName = match[1]?.trim();
|
|
212
|
+
if (entityName && entityName.length >= 3 && entityName.length <= 100) {
|
|
213
|
+
entities.add(entityName);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return Array.from(entities);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extract relationships between entities
|
|
223
|
+
*/
|
|
224
|
+
function extractRelationships(text) {
|
|
225
|
+
const relationships = [];
|
|
226
|
+
const patterns = EXTRACTION_PATTERNS.relationship;
|
|
227
|
+
|
|
228
|
+
for (const pattern of patterns) {
|
|
229
|
+
let match;
|
|
230
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
231
|
+
const from = match[1]?.trim();
|
|
232
|
+
const to = match[2]?.trim();
|
|
233
|
+
if (from && to) {
|
|
234
|
+
relationships.push({
|
|
235
|
+
from,
|
|
236
|
+
to,
|
|
237
|
+
relationship: 'depends_on', // Generalized relationship type
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return relationships;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Classify content category
|
|
248
|
+
*/
|
|
249
|
+
function classifyCategory(content) {
|
|
250
|
+
const lower = content.toLowerCase();
|
|
251
|
+
|
|
252
|
+
if (lower.includes('code') || lower.includes('function') || lower.includes('file') ||
|
|
253
|
+
lower.includes('class') || lower.includes('module')) {
|
|
254
|
+
return 'code';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (lower.includes('project') || lower.includes('application') || lower.includes('system')) {
|
|
258
|
+
return 'project';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (lower.includes('user') || lower.includes('prefer') || lower.includes('like')) {
|
|
262
|
+
return 'user';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return 'general';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create memory with surprise score calculation
|
|
270
|
+
*/
|
|
271
|
+
async function createMemoryWithSurprise(options) {
|
|
272
|
+
const { content, type, category, sessionId, userContent, metadata = {} } = options;
|
|
273
|
+
|
|
274
|
+
// Get existing memories for surprise calculation
|
|
275
|
+
const existingMemories = store.getRecentMemories({ limit: 100, sessionId });
|
|
276
|
+
|
|
277
|
+
// Calculate surprise score
|
|
278
|
+
const surpriseScore = surprise.calculateSurprise({
|
|
279
|
+
content,
|
|
280
|
+
type,
|
|
281
|
+
category,
|
|
282
|
+
}, existingMemories, { userContent });
|
|
283
|
+
|
|
284
|
+
// Only store if surprise score exceeds threshold
|
|
285
|
+
const threshold = config.memory?.surpriseThreshold ?? 0.3;
|
|
286
|
+
if (surpriseScore < threshold) {
|
|
287
|
+
logger.debug({ content, surpriseScore, threshold }, 'Memory filtered by surprise threshold');
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Calculate initial importance based on surprise and type
|
|
292
|
+
const importance = calculateInitialImportance(type, surpriseScore);
|
|
293
|
+
|
|
294
|
+
// Store memory
|
|
295
|
+
try {
|
|
296
|
+
const memory = store.createMemory({
|
|
297
|
+
sessionId,
|
|
298
|
+
content,
|
|
299
|
+
type,
|
|
300
|
+
category,
|
|
301
|
+
importance,
|
|
302
|
+
surpriseScore,
|
|
303
|
+
metadata: {
|
|
304
|
+
...metadata,
|
|
305
|
+
extractedAt: Date.now(),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return memory;
|
|
310
|
+
} catch (err) {
|
|
311
|
+
logger.warn({ err, content }, 'Failed to store memory');
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Calculate initial importance score
|
|
318
|
+
*/
|
|
319
|
+
function calculateInitialImportance(type, surpriseScore) {
|
|
320
|
+
// Base importance by type
|
|
321
|
+
const baseImportance = {
|
|
322
|
+
preference: 0.7, // User preferences are important
|
|
323
|
+
decision: 0.8, // Decisions are very important
|
|
324
|
+
fact: 0.6, // Facts are moderately important
|
|
325
|
+
entity: 0.4, // Entities are less important individually
|
|
326
|
+
relationship: 0.5,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const base = baseImportance[type] ?? 0.5;
|
|
330
|
+
|
|
331
|
+
// Boost by surprise score (0-1 scale)
|
|
332
|
+
return Math.min(1.0, base + (surpriseScore * 0.3));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Parse entities from content
|
|
337
|
+
*/
|
|
338
|
+
function parseEntities(content) {
|
|
339
|
+
return extractEntities(content);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
extractMemories,
|
|
344
|
+
extractContent,
|
|
345
|
+
extractByType,
|
|
346
|
+
extractEntities,
|
|
347
|
+
extractRelationships,
|
|
348
|
+
parseEntities,
|
|
349
|
+
classifyCategory,
|
|
350
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Titans-Inspired Long-Term Memory System
|
|
3
|
+
*
|
|
4
|
+
* This module provides long-term memory capabilities with:
|
|
5
|
+
* - Surprise-based memory updates (Titans core innovation)
|
|
6
|
+
* - Automatic memory extraction from conversations
|
|
7
|
+
* - FTS5 semantic search
|
|
8
|
+
* - Multi-signal memory retrieval
|
|
9
|
+
* - Memory management tools
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const store = require("./store");
|
|
13
|
+
const search = require("./search");
|
|
14
|
+
const retriever = require("./retriever");
|
|
15
|
+
const extractor = require("./extractor");
|
|
16
|
+
const surprise = require("./surprise");
|
|
17
|
+
const tools = require("./tools");
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
// Store operations
|
|
21
|
+
store,
|
|
22
|
+
createMemory: store.createMemory,
|
|
23
|
+
getMemory: store.getMemory,
|
|
24
|
+
updateMemory: store.updateMemory,
|
|
25
|
+
deleteMemory: store.deleteMemory,
|
|
26
|
+
getRecentMemories: store.getRecentMemories,
|
|
27
|
+
getMemoriesByImportance: store.getMemoriesByImportance,
|
|
28
|
+
pruneOldMemories: store.pruneOldMemories,
|
|
29
|
+
countMemories: store.countMemories,
|
|
30
|
+
|
|
31
|
+
// Search operations
|
|
32
|
+
search,
|
|
33
|
+
searchMemories: search.searchMemories,
|
|
34
|
+
searchWithExpansion: search.searchWithExpansion,
|
|
35
|
+
findSimilar: search.findSimilar,
|
|
36
|
+
|
|
37
|
+
// Retrieval
|
|
38
|
+
retriever,
|
|
39
|
+
retrieveRelevantMemories: retriever.retrieveRelevantMemories,
|
|
40
|
+
formatMemoriesForContext: retriever.formatMemoriesForContext,
|
|
41
|
+
injectMemoriesIntoSystem: retriever.injectMemoriesIntoSystem,
|
|
42
|
+
getMemoryStats: retriever.getMemoryStats,
|
|
43
|
+
|
|
44
|
+
// Extraction
|
|
45
|
+
extractor,
|
|
46
|
+
extractMemories: extractor.extractMemories,
|
|
47
|
+
|
|
48
|
+
// Surprise detection
|
|
49
|
+
surprise,
|
|
50
|
+
calculateSurprise: surprise.calculateSurprise,
|
|
51
|
+
|
|
52
|
+
// Tools
|
|
53
|
+
tools,
|
|
54
|
+
MEMORY_TOOLS: tools.MEMORY_TOOLS,
|
|
55
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
const store = require("./store");
|
|
2
|
+
const search = require("./search");
|
|
3
|
+
const logger = require("../logger");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieve relevant memories using multi-signal ranking
|
|
7
|
+
*
|
|
8
|
+
* Scoring algorithm:
|
|
9
|
+
* - 30% Recency: Exponential decay based on last access
|
|
10
|
+
* - 40% Importance: Stored importance value
|
|
11
|
+
* - 30% Relevance: Keyword overlap with query
|
|
12
|
+
*/
|
|
13
|
+
function retrieveRelevantMemories(query, options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
limit = 10,
|
|
16
|
+
sessionId = null,
|
|
17
|
+
includeGlobal = true,
|
|
18
|
+
recencyWeight = 0.3,
|
|
19
|
+
importanceWeight = 0.4,
|
|
20
|
+
relevanceWeight = 0.3,
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// 1. FTS5 search for keyword relevance
|
|
25
|
+
const ftsResults = search.searchMemories({
|
|
26
|
+
query,
|
|
27
|
+
limit: limit * 3, // Get more candidates
|
|
28
|
+
sessionId: includeGlobal ? null : sessionId,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 2. Get recent memories (recency bias)
|
|
32
|
+
const recentMemories = store.getRecentMemories({
|
|
33
|
+
limit: limit * 2,
|
|
34
|
+
sessionId: includeGlobal ? null : sessionId,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 3. Get high-importance memories
|
|
38
|
+
const importantMemories = store.getMemoriesByImportance({
|
|
39
|
+
limit: limit * 2,
|
|
40
|
+
sessionId: includeGlobal ? null : sessionId,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 4. Merge and deduplicate
|
|
44
|
+
const candidates = mergeUnique([ftsResults, recentMemories, importantMemories]);
|
|
45
|
+
|
|
46
|
+
// 5. Score and rank
|
|
47
|
+
const scored = candidates.map(memory => ({
|
|
48
|
+
memory,
|
|
49
|
+
score: calculateRetrievalScore(memory, query, {
|
|
50
|
+
recencyWeight,
|
|
51
|
+
importanceWeight,
|
|
52
|
+
relevanceWeight,
|
|
53
|
+
}),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// 6. Sort by score and return top K
|
|
57
|
+
const topMemories = scored
|
|
58
|
+
.sort((a, b) => b.score - a.score)
|
|
59
|
+
.slice(0, limit)
|
|
60
|
+
.map(s => s.memory);
|
|
61
|
+
|
|
62
|
+
// 7. Update access counts asynchronously
|
|
63
|
+
setImmediate(() => {
|
|
64
|
+
for (const memory of topMemories) {
|
|
65
|
+
try {
|
|
66
|
+
store.incrementAccessCount(memory.id);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
logger.warn({ err, memoryId: memory.id }, 'Failed to increment access count');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return topMemories;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.error({ err, query }, 'Memory retrieval failed');
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calculate retrieval score for a memory
|
|
82
|
+
*/
|
|
83
|
+
function calculateRetrievalScore(memory, query, weights) {
|
|
84
|
+
// Recency score: exponential decay based on last access
|
|
85
|
+
const ageMs = Date.now() - (memory.lastAccessedAt || memory.createdAt);
|
|
86
|
+
const halfLifeMs = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
87
|
+
const recencyScore = Math.exp(-ageMs / halfLifeMs);
|
|
88
|
+
|
|
89
|
+
// Importance score: direct from stored value
|
|
90
|
+
const importanceScore = memory.importance ?? 0.5;
|
|
91
|
+
|
|
92
|
+
// Relevance score: keyword overlap with query
|
|
93
|
+
const relevanceScore = calculateKeywordOverlap(memory.content, query);
|
|
94
|
+
|
|
95
|
+
// Weighted combination
|
|
96
|
+
return (
|
|
97
|
+
weights.recencyWeight * recencyScore +
|
|
98
|
+
weights.importanceWeight * importanceScore +
|
|
99
|
+
weights.relevanceWeight * relevanceScore
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Calculate keyword overlap between content and query
|
|
105
|
+
*/
|
|
106
|
+
function calculateKeywordOverlap(content, query) {
|
|
107
|
+
const contentKeywords = new Set(search.extractKeywords(content));
|
|
108
|
+
const queryKeywords = search.extractKeywords(query);
|
|
109
|
+
|
|
110
|
+
if (queryKeywords.length === 0 || contentKeywords.size === 0) {
|
|
111
|
+
return 0.0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let overlapCount = 0;
|
|
115
|
+
for (const keyword of queryKeywords) {
|
|
116
|
+
if (contentKeywords.has(keyword)) {
|
|
117
|
+
overlapCount++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return overlapCount / queryKeywords.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Merge arrays and remove duplicates by memory ID
|
|
126
|
+
*/
|
|
127
|
+
function mergeUnique(arrays) {
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const merged = [];
|
|
130
|
+
|
|
131
|
+
for (const arr of arrays) {
|
|
132
|
+
for (const item of arr) {
|
|
133
|
+
if (!seen.has(item.id)) {
|
|
134
|
+
seen.add(item.id);
|
|
135
|
+
merged.push(item);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return merged;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract query from message (handle different formats)
|
|
145
|
+
*/
|
|
146
|
+
function extractQueryFromMessage(message) {
|
|
147
|
+
if (!message) return '';
|
|
148
|
+
|
|
149
|
+
if (typeof message === 'string') return message;
|
|
150
|
+
|
|
151
|
+
if (message.content) {
|
|
152
|
+
if (typeof message.content === 'string') return message.content;
|
|
153
|
+
|
|
154
|
+
if (Array.isArray(message.content)) {
|
|
155
|
+
return message.content
|
|
156
|
+
.filter(block => block?.type === 'text' || typeof block === 'string')
|
|
157
|
+
.map(block => typeof block === 'string' ? block : block.text)
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join(' ');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Format memories for injection into context
|
|
168
|
+
*/
|
|
169
|
+
function formatMemoriesForContext(memories) {
|
|
170
|
+
if (!memories || memories.length === 0) return '';
|
|
171
|
+
|
|
172
|
+
return memories
|
|
173
|
+
.map((memory, index) => {
|
|
174
|
+
const age = formatAge(Date.now() - memory.createdAt);
|
|
175
|
+
const typeLabel = memory.type || 'memory';
|
|
176
|
+
return `${index + 1}. [${typeLabel}] ${memory.content} (${age})`;
|
|
177
|
+
})
|
|
178
|
+
.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Format age in human-readable form
|
|
183
|
+
*/
|
|
184
|
+
function formatAge(ageMs) {
|
|
185
|
+
const seconds = Math.floor(ageMs / 1000);
|
|
186
|
+
const minutes = Math.floor(seconds / 60);
|
|
187
|
+
const hours = Math.floor(minutes / 60);
|
|
188
|
+
const days = Math.floor(hours / 24);
|
|
189
|
+
const weeks = Math.floor(days / 7);
|
|
190
|
+
|
|
191
|
+
if (weeks > 0) return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
|
|
192
|
+
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
193
|
+
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
194
|
+
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
195
|
+
return 'just now';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Inject memories into system prompt
|
|
200
|
+
*/
|
|
201
|
+
function injectMemoriesIntoSystem(existingSystem, memories, format = 'system') {
|
|
202
|
+
if (!memories || memories.length === 0) return existingSystem;
|
|
203
|
+
|
|
204
|
+
const formattedMemories = formatMemoriesForContext(memories);
|
|
205
|
+
|
|
206
|
+
if (format === 'system') {
|
|
207
|
+
const memoryBlock = `
|
|
208
|
+
<long_term_memory>
|
|
209
|
+
The following are relevant facts and context from previous conversations:
|
|
210
|
+
${formattedMemories}
|
|
211
|
+
</long_term_memory>`;
|
|
212
|
+
|
|
213
|
+
return existingSystem
|
|
214
|
+
? `${existingSystem}\n${memoryBlock}`
|
|
215
|
+
: memoryBlock;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (format === 'assistant_preamble') {
|
|
219
|
+
return {
|
|
220
|
+
system: existingSystem,
|
|
221
|
+
memoryPreamble: formattedMemories,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return existingSystem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get memory statistics
|
|
230
|
+
*/
|
|
231
|
+
function getMemoryStats(sessionId = null) {
|
|
232
|
+
try {
|
|
233
|
+
const total = store.countMemories();
|
|
234
|
+
const byType = {};
|
|
235
|
+
const types = ['preference', 'decision', 'fact', 'entity', 'relationship'];
|
|
236
|
+
|
|
237
|
+
for (const type of types) {
|
|
238
|
+
byType[type] = store.getMemoriesByType(type, 1000).length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const recent = store.getRecentMemories({ limit: 10, sessionId });
|
|
242
|
+
const important = store.getMemoriesByImportance({ limit: 10, sessionId });
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
total,
|
|
246
|
+
byType,
|
|
247
|
+
recentCount: recent.length,
|
|
248
|
+
importantCount: important.length,
|
|
249
|
+
sessionId,
|
|
250
|
+
};
|
|
251
|
+
} catch (err) {
|
|
252
|
+
logger.error({ err }, 'Failed to get memory stats');
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
retrieveRelevantMemories,
|
|
259
|
+
calculateRetrievalScore,
|
|
260
|
+
calculateKeywordOverlap,
|
|
261
|
+
extractQueryFromMessage,
|
|
262
|
+
formatMemoriesForContext,
|
|
263
|
+
injectMemoriesIntoSystem,
|
|
264
|
+
formatAge,
|
|
265
|
+
getMemoryStats,
|
|
266
|
+
};
|