cozo-memory 1.1.6 → 1.1.7
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/dist/memory-activation.js +295 -0
- package/dist/query-aware-traversal.js +291 -0
- package/dist/query-pipeline.js +355 -0
- package/dist/test-memory-activation.js +222 -0
- package/dist/test-query-aware-traversal.js +226 -0
- package/dist/test-query-pipeline.js +151 -0
- package/package.json +1 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PipelineBuilder = exports.QueryPipeline = exports.postProcessStages = exports.rerankStages = exports.searchStages = exports.preprocessStages = void 0;
|
|
4
|
+
exports.createStandardPipeline = createStandardPipeline;
|
|
5
|
+
exports.createGraphRagPipeline = createGraphRagPipeline;
|
|
6
|
+
exports.createAgenticPipeline = createAgenticPipeline;
|
|
7
|
+
// Built-in Preprocessing Stages
|
|
8
|
+
exports.preprocessStages = {
|
|
9
|
+
embedQuery: (embeddingService) => ({
|
|
10
|
+
type: 'preprocess',
|
|
11
|
+
name: 'embed-query',
|
|
12
|
+
enabled: true,
|
|
13
|
+
execute: async (ctx) => {
|
|
14
|
+
const start = Date.now();
|
|
15
|
+
ctx.embedding = await embeddingService.embed(ctx.query);
|
|
16
|
+
ctx.metrics['preprocess.embedding'] = Date.now() - start;
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
|
19
|
+
}),
|
|
20
|
+
queryNormalization: () => ({
|
|
21
|
+
type: 'preprocess',
|
|
22
|
+
name: 'query-normalization',
|
|
23
|
+
enabled: true,
|
|
24
|
+
execute: async (ctx) => {
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
ctx.query = ctx.query.trim().toLowerCase();
|
|
27
|
+
ctx.metrics['preprocess.normalization'] = Date.now() - start;
|
|
28
|
+
return ctx;
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
// Built-in Search Stages
|
|
33
|
+
exports.searchStages = {
|
|
34
|
+
hybridSearch: (hybridSearch) => ({
|
|
35
|
+
type: 'search',
|
|
36
|
+
name: 'hybrid-search',
|
|
37
|
+
enabled: true,
|
|
38
|
+
params: { limit: 10 },
|
|
39
|
+
execute: async (ctx) => {
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
const limit = ctx.metadata.limit || 10;
|
|
42
|
+
ctx.results = await hybridSearch.search({
|
|
43
|
+
query: ctx.query,
|
|
44
|
+
limit,
|
|
45
|
+
includeEntities: true,
|
|
46
|
+
includeObservations: true
|
|
47
|
+
});
|
|
48
|
+
ctx.metrics['search.hybrid'] = Date.now() - start;
|
|
49
|
+
return ctx;
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
graphRag: (hybridSearch) => ({
|
|
53
|
+
type: 'search',
|
|
54
|
+
name: 'graph-rag',
|
|
55
|
+
enabled: true,
|
|
56
|
+
params: { maxDepth: 3, limit: 10 },
|
|
57
|
+
execute: async (ctx) => {
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
const limit = ctx.metadata.limit || 10;
|
|
60
|
+
ctx.results = await hybridSearch.graphRag({
|
|
61
|
+
query: ctx.query,
|
|
62
|
+
limit,
|
|
63
|
+
graphConstraints: {
|
|
64
|
+
maxDepth: ctx.metadata.maxDepth || 3
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
ctx.metrics['search.graphRag'] = Date.now() - start;
|
|
68
|
+
return ctx;
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
agenticSearch: (hybridSearch) => ({
|
|
72
|
+
type: 'search',
|
|
73
|
+
name: 'agentic-search',
|
|
74
|
+
enabled: true,
|
|
75
|
+
params: { limit: 10 },
|
|
76
|
+
execute: async (ctx) => {
|
|
77
|
+
const start = Date.now();
|
|
78
|
+
const limit = ctx.metadata.limit || 10;
|
|
79
|
+
ctx.results = await hybridSearch.agenticRetrieve({
|
|
80
|
+
query: ctx.query,
|
|
81
|
+
limit
|
|
82
|
+
});
|
|
83
|
+
ctx.metrics['search.agentic'] = Date.now() - start;
|
|
84
|
+
return ctx;
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
};
|
|
88
|
+
// Built-in Reranking Stages
|
|
89
|
+
exports.rerankStages = {
|
|
90
|
+
crossEncoder: (reranker) => ({
|
|
91
|
+
type: 'rerank',
|
|
92
|
+
name: 'cross-encoder',
|
|
93
|
+
enabled: true,
|
|
94
|
+
execute: async (ctx) => {
|
|
95
|
+
const start = Date.now();
|
|
96
|
+
if (!ctx.results || ctx.results.length === 0) {
|
|
97
|
+
return ctx;
|
|
98
|
+
}
|
|
99
|
+
ctx.results = await reranker.rerank(ctx.query, ctx.results);
|
|
100
|
+
ctx.metrics['rerank.crossEncoder'] = Date.now() - start;
|
|
101
|
+
return ctx;
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
diversityRerank: () => ({
|
|
105
|
+
type: 'rerank',
|
|
106
|
+
name: 'diversity-rerank',
|
|
107
|
+
enabled: true,
|
|
108
|
+
params: { diversityWeight: 0.3 },
|
|
109
|
+
execute: async (ctx) => {
|
|
110
|
+
const start = Date.now();
|
|
111
|
+
if (!ctx.results || ctx.results.length === 0) {
|
|
112
|
+
return ctx;
|
|
113
|
+
}
|
|
114
|
+
// MMR-style diversity reranking
|
|
115
|
+
const diversityWeight = ctx.metadata.diversityWeight || 0.3;
|
|
116
|
+
const reranked = [];
|
|
117
|
+
const remaining = [...ctx.results];
|
|
118
|
+
while (remaining.length > 0 && reranked.length < ctx.results.length) {
|
|
119
|
+
let bestIdx = 0;
|
|
120
|
+
let bestScore = -Infinity;
|
|
121
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
122
|
+
const candidate = remaining[i];
|
|
123
|
+
const relevance = candidate.score || 0;
|
|
124
|
+
// Calculate diversity (min similarity to already selected)
|
|
125
|
+
let minSim = 1.0;
|
|
126
|
+
for (const selected of reranked) {
|
|
127
|
+
const sim = cosineSimilarity(candidate.embedding || [], selected.embedding || []);
|
|
128
|
+
minSim = Math.min(minSim, sim);
|
|
129
|
+
}
|
|
130
|
+
const score = (1 - diversityWeight) * relevance + diversityWeight * (1 - minSim);
|
|
131
|
+
if (score > bestScore) {
|
|
132
|
+
bestScore = score;
|
|
133
|
+
bestIdx = i;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
reranked.push(remaining.splice(bestIdx, 1)[0]);
|
|
137
|
+
}
|
|
138
|
+
ctx.results = reranked;
|
|
139
|
+
ctx.metrics['rerank.diversity'] = Date.now() - start;
|
|
140
|
+
return ctx;
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
// Built-in Post-processing Stages
|
|
145
|
+
exports.postProcessStages = {
|
|
146
|
+
deduplication: () => ({
|
|
147
|
+
type: 'postprocess',
|
|
148
|
+
name: 'deduplication',
|
|
149
|
+
enabled: true,
|
|
150
|
+
params: { threshold: 0.95 },
|
|
151
|
+
execute: async (ctx) => {
|
|
152
|
+
const start = Date.now();
|
|
153
|
+
if (!ctx.results || ctx.results.length === 0) {
|
|
154
|
+
return ctx;
|
|
155
|
+
}
|
|
156
|
+
const threshold = ctx.metadata.dedupThreshold || 0.95;
|
|
157
|
+
const deduplicated = [];
|
|
158
|
+
for (const result of ctx.results) {
|
|
159
|
+
let isDuplicate = false;
|
|
160
|
+
for (const existing of deduplicated) {
|
|
161
|
+
if (result.entity_id === existing.entity_id) {
|
|
162
|
+
isDuplicate = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
const sim = cosineSimilarity(result.embedding || [], existing.embedding || []);
|
|
166
|
+
if (sim >= threshold) {
|
|
167
|
+
isDuplicate = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!isDuplicate) {
|
|
172
|
+
deduplicated.push(result);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
ctx.results = deduplicated;
|
|
176
|
+
ctx.metrics['postprocess.deduplication'] = Date.now() - start;
|
|
177
|
+
return ctx;
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
scoreNormalization: () => ({
|
|
181
|
+
type: 'postprocess',
|
|
182
|
+
name: 'score-normalization',
|
|
183
|
+
enabled: true,
|
|
184
|
+
execute: async (ctx) => {
|
|
185
|
+
const start = Date.now();
|
|
186
|
+
if (!ctx.results || ctx.results.length === 0) {
|
|
187
|
+
return ctx;
|
|
188
|
+
}
|
|
189
|
+
const scores = ctx.results.map(r => r.score || 0);
|
|
190
|
+
const maxScore = Math.max(...scores);
|
|
191
|
+
const minScore = Math.min(...scores);
|
|
192
|
+
const range = maxScore - minScore;
|
|
193
|
+
if (range > 0) {
|
|
194
|
+
ctx.results = ctx.results.map(r => ({
|
|
195
|
+
...r,
|
|
196
|
+
score: (r.score - minScore) / range
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
ctx.metrics['postprocess.normalization'] = Date.now() - start;
|
|
200
|
+
return ctx;
|
|
201
|
+
}
|
|
202
|
+
}),
|
|
203
|
+
topK: () => ({
|
|
204
|
+
type: 'postprocess',
|
|
205
|
+
name: 'top-k',
|
|
206
|
+
enabled: true,
|
|
207
|
+
params: { k: 10 },
|
|
208
|
+
execute: async (ctx) => {
|
|
209
|
+
const start = Date.now();
|
|
210
|
+
const k = ctx.metadata.topK || 10;
|
|
211
|
+
if (ctx.results && ctx.results.length > k) {
|
|
212
|
+
ctx.results = ctx.results.slice(0, k);
|
|
213
|
+
}
|
|
214
|
+
ctx.metrics['postprocess.topK'] = Date.now() - start;
|
|
215
|
+
return ctx;
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
};
|
|
219
|
+
// Pipeline Executor
|
|
220
|
+
class QueryPipeline {
|
|
221
|
+
config;
|
|
222
|
+
constructor(config) {
|
|
223
|
+
this.config = config;
|
|
224
|
+
}
|
|
225
|
+
async execute(query, metadata = {}) {
|
|
226
|
+
const ctx = {
|
|
227
|
+
query,
|
|
228
|
+
metadata: { ...metadata },
|
|
229
|
+
metrics: {},
|
|
230
|
+
results: []
|
|
231
|
+
};
|
|
232
|
+
const pipelineStart = Date.now();
|
|
233
|
+
for (const stage of this.config.stages) {
|
|
234
|
+
if (!stage.enabled) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
// Check condition if present
|
|
238
|
+
if (stage.condition && !stage.condition(ctx)) {
|
|
239
|
+
console.log(`[Pipeline] Skipping stage ${stage.name} (condition not met)`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const stageStart = Date.now();
|
|
244
|
+
await stage.execute(ctx);
|
|
245
|
+
ctx.metrics[`stage.${stage.name}`] = Date.now() - stageStart;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error(`[Pipeline] Error in stage ${stage.name}:`, error);
|
|
249
|
+
ctx.metadata[`error.${stage.name}`] = String(error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
ctx.metrics['pipeline.total'] = Date.now() - pipelineStart;
|
|
253
|
+
return {
|
|
254
|
+
results: ctx.results || [],
|
|
255
|
+
metrics: ctx.metrics,
|
|
256
|
+
metadata: ctx.metadata
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// A/B Testing support
|
|
260
|
+
async executeWithVariants(query, metadata = {}) {
|
|
261
|
+
if (!this.config.abTest?.enabled) {
|
|
262
|
+
return { primary: await this.execute(query, metadata) };
|
|
263
|
+
}
|
|
264
|
+
const primary = await this.execute(query, metadata);
|
|
265
|
+
// Execute variant pipelines if configured
|
|
266
|
+
const variants = {};
|
|
267
|
+
if (this.config.abTest.variants) {
|
|
268
|
+
for (const variant of this.config.abTest.variants) {
|
|
269
|
+
variants[variant] = await this.execute(query, { ...metadata, variant });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { primary, variants };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
exports.QueryPipeline = QueryPipeline;
|
|
276
|
+
// Helper: Cosine Similarity
|
|
277
|
+
function cosineSimilarity(a, b) {
|
|
278
|
+
if (a.length !== b.length || a.length === 0)
|
|
279
|
+
return 0;
|
|
280
|
+
let dotProduct = 0;
|
|
281
|
+
let normA = 0;
|
|
282
|
+
let normB = 0;
|
|
283
|
+
for (let i = 0; i < a.length; i++) {
|
|
284
|
+
dotProduct += a[i] * b[i];
|
|
285
|
+
normA += a[i] * a[i];
|
|
286
|
+
normB += b[i] * b[i];
|
|
287
|
+
}
|
|
288
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
289
|
+
return denominator === 0 ? 0 : dotProduct / denominator;
|
|
290
|
+
}
|
|
291
|
+
// Pipeline Builder for easy construction
|
|
292
|
+
class PipelineBuilder {
|
|
293
|
+
stages = [];
|
|
294
|
+
name;
|
|
295
|
+
constructor(name) {
|
|
296
|
+
this.name = name;
|
|
297
|
+
}
|
|
298
|
+
addStage(stage) {
|
|
299
|
+
this.stages.push(stage);
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
addPreprocess(stage) {
|
|
303
|
+
return this.addStage(stage);
|
|
304
|
+
}
|
|
305
|
+
addSearch(stage) {
|
|
306
|
+
return this.addStage(stage);
|
|
307
|
+
}
|
|
308
|
+
addRerank(stage) {
|
|
309
|
+
return this.addStage(stage);
|
|
310
|
+
}
|
|
311
|
+
addPostProcess(stage) {
|
|
312
|
+
return this.addStage(stage);
|
|
313
|
+
}
|
|
314
|
+
build() {
|
|
315
|
+
return {
|
|
316
|
+
name: this.name,
|
|
317
|
+
stages: this.stages
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.PipelineBuilder = PipelineBuilder;
|
|
322
|
+
// Preset Pipelines
|
|
323
|
+
function createStandardPipeline(hybridSearch, embeddingService, reranker) {
|
|
324
|
+
const config = new PipelineBuilder('standard')
|
|
325
|
+
.addPreprocess(exports.preprocessStages.queryNormalization())
|
|
326
|
+
.addPreprocess(exports.preprocessStages.embedQuery(embeddingService))
|
|
327
|
+
.addSearch(exports.searchStages.hybridSearch(hybridSearch))
|
|
328
|
+
.addRerank(exports.rerankStages.crossEncoder(reranker))
|
|
329
|
+
.addPostProcess(exports.postProcessStages.deduplication())
|
|
330
|
+
.addPostProcess(exports.postProcessStages.topK())
|
|
331
|
+
.build();
|
|
332
|
+
return new QueryPipeline(config);
|
|
333
|
+
}
|
|
334
|
+
function createGraphRagPipeline(hybridSearch, embeddingService) {
|
|
335
|
+
const config = new PipelineBuilder('graph-rag')
|
|
336
|
+
.addPreprocess(exports.preprocessStages.embedQuery(embeddingService))
|
|
337
|
+
.addSearch(exports.searchStages.graphRag(hybridSearch))
|
|
338
|
+
.addRerank(exports.rerankStages.diversityRerank())
|
|
339
|
+
.addPostProcess(exports.postProcessStages.deduplication())
|
|
340
|
+
.addPostProcess(exports.postProcessStages.scoreNormalization())
|
|
341
|
+
.addPostProcess(exports.postProcessStages.topK())
|
|
342
|
+
.build();
|
|
343
|
+
return new QueryPipeline(config);
|
|
344
|
+
}
|
|
345
|
+
function createAgenticPipeline(hybridSearch, embeddingService, reranker) {
|
|
346
|
+
const config = new PipelineBuilder('agentic')
|
|
347
|
+
.addPreprocess(exports.preprocessStages.queryNormalization())
|
|
348
|
+
.addPreprocess(exports.preprocessStages.embedQuery(embeddingService))
|
|
349
|
+
.addSearch(exports.searchStages.agenticSearch(hybridSearch))
|
|
350
|
+
.addRerank(exports.rerankStages.crossEncoder(reranker))
|
|
351
|
+
.addPostProcess(exports.postProcessStages.deduplication())
|
|
352
|
+
.addPostProcess(exports.postProcessStages.topK())
|
|
353
|
+
.build();
|
|
354
|
+
return new QueryPipeline(config);
|
|
355
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const cozo_node_1 = require("cozo-node");
|
|
4
|
+
const memory_activation_1 = require("./memory-activation");
|
|
5
|
+
async function testMemoryActivation() {
|
|
6
|
+
console.log('=== Memory Activation Service Test ===\n');
|
|
7
|
+
const db = new cozo_node_1.CozoDb();
|
|
8
|
+
const activationService = new memory_activation_1.MemoryActivationService(db, {
|
|
9
|
+
retentionThreshold: 0.1,
|
|
10
|
+
initialStrength: 1.0,
|
|
11
|
+
strengthIncrement: 1.0,
|
|
12
|
+
timeUnit: 'days'
|
|
13
|
+
});
|
|
14
|
+
try {
|
|
15
|
+
// Setup: Create test entity and observations
|
|
16
|
+
console.log('--- Setup: Creating test data ---');
|
|
17
|
+
const entityId = 'test-entity-1';
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const oneDayAgo = now - (24 * 60 * 60 * 1000);
|
|
20
|
+
const threeDaysAgo = now - (3 * 24 * 60 * 60 * 1000);
|
|
21
|
+
const sevenDaysAgo = now - (7 * 24 * 60 * 60 * 1000);
|
|
22
|
+
const thirtyDaysAgo = now - (30 * 24 * 60 * 60 * 1000);
|
|
23
|
+
// Create entity relation
|
|
24
|
+
await db.run(`
|
|
25
|
+
:create entity {
|
|
26
|
+
id: String,
|
|
27
|
+
name: String,
|
|
28
|
+
type: String,
|
|
29
|
+
=>
|
|
30
|
+
metadata: String
|
|
31
|
+
}
|
|
32
|
+
`);
|
|
33
|
+
await db.run(`
|
|
34
|
+
?[id, name, type, metadata] <- [
|
|
35
|
+
[$id, 'Test Entity', 'test', 'test entity']
|
|
36
|
+
]
|
|
37
|
+
:put entity {id, name, type => metadata}
|
|
38
|
+
`, { id: entityId });
|
|
39
|
+
// Create observation relation
|
|
40
|
+
await db.run(`
|
|
41
|
+
:create observation {
|
|
42
|
+
id: String,
|
|
43
|
+
entity_id: String,
|
|
44
|
+
text: String,
|
|
45
|
+
=>
|
|
46
|
+
metadata: Any,
|
|
47
|
+
created_at: Int
|
|
48
|
+
}
|
|
49
|
+
`);
|
|
50
|
+
// Create observations with different access patterns
|
|
51
|
+
const observations = [
|
|
52
|
+
{
|
|
53
|
+
id: 'obs-1',
|
|
54
|
+
text: 'Recently accessed, high strength',
|
|
55
|
+
metadata: { access_count: 5, last_access_time: oneDayAgo },
|
|
56
|
+
created_at: thirtyDaysAgo
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'obs-2',
|
|
60
|
+
text: 'Moderately accessed',
|
|
61
|
+
metadata: { access_count: 2, last_access_time: threeDaysAgo },
|
|
62
|
+
created_at: thirtyDaysAgo
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'obs-3',
|
|
66
|
+
text: 'Rarely accessed, weak memory',
|
|
67
|
+
metadata: { access_count: 1, last_access_time: sevenDaysAgo },
|
|
68
|
+
created_at: thirtyDaysAgo
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'obs-4',
|
|
72
|
+
text: 'Never accessed, very weak',
|
|
73
|
+
metadata: { access_count: 0, last_access_time: thirtyDaysAgo },
|
|
74
|
+
created_at: thirtyDaysAgo
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'obs-5',
|
|
78
|
+
text: 'Frequently accessed, very strong',
|
|
79
|
+
metadata: { access_count: 10, last_access_time: now },
|
|
80
|
+
created_at: thirtyDaysAgo
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
for (const obs of observations) {
|
|
84
|
+
await db.run(`
|
|
85
|
+
?[id, entity_id, text, metadata, created_at] <- [
|
|
86
|
+
[$id, $entity_id, $text, $metadata, $created_at]
|
|
87
|
+
]
|
|
88
|
+
:put observation {id, entity_id, text => metadata, created_at}
|
|
89
|
+
`, {
|
|
90
|
+
id: obs.id,
|
|
91
|
+
entity_id: entityId,
|
|
92
|
+
text: obs.text,
|
|
93
|
+
metadata: obs.metadata,
|
|
94
|
+
created_at: obs.created_at
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
console.log(`✓ Created ${observations.length} test observations\n`);
|
|
98
|
+
// Test 1: Calculate activation scores
|
|
99
|
+
console.log('--- Test 1: Calculate Activation Scores ---');
|
|
100
|
+
const scores = await activationService.calculateActivationScores(entityId);
|
|
101
|
+
console.log('Activation Scores (sorted by activation):');
|
|
102
|
+
for (const score of scores) {
|
|
103
|
+
console.log(` ${score.observationId}:`);
|
|
104
|
+
console.log(` Activation: ${score.activation.toFixed(4)}`);
|
|
105
|
+
console.log(` Strength: ${score.strength.toFixed(2)}`);
|
|
106
|
+
console.log(` Time since access: ${score.timeSinceAccess.toFixed(2)} days`);
|
|
107
|
+
console.log(` Access count: ${score.accessCount}`);
|
|
108
|
+
console.log(` Should retain: ${score.shouldRetain}`);
|
|
109
|
+
console.log(` Reason: ${score.reason}`);
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
// Test 2: Get activation statistics
|
|
113
|
+
console.log('--- Test 2: Activation Statistics ---');
|
|
114
|
+
const stats = await activationService.getActivationStats(entityId);
|
|
115
|
+
console.log(`Total observations: ${stats.totalObservations}`);
|
|
116
|
+
console.log(`Average activation: ${stats.averageActivation.toFixed(4)}`);
|
|
117
|
+
console.log(`Average strength: ${stats.averageStrength.toFixed(2)}`);
|
|
118
|
+
console.log(`Below threshold: ${stats.belowThreshold}`);
|
|
119
|
+
console.log(`Above threshold: ${stats.aboveThreshold}`);
|
|
120
|
+
console.log('Distribution:');
|
|
121
|
+
console.log(` Very weak (<0.1): ${stats.distribution.veryWeak}`);
|
|
122
|
+
console.log(` Weak (0.1-0.3): ${stats.distribution.weak}`);
|
|
123
|
+
console.log(` Moderate (0.3-0.6): ${stats.distribution.moderate}`);
|
|
124
|
+
console.log(` Strong (0.6-0.9): ${stats.distribution.strong}`);
|
|
125
|
+
console.log(` Very strong (>0.9): ${stats.distribution.veryStrong}`);
|
|
126
|
+
console.log();
|
|
127
|
+
// Test 3: Record access (simulates recall)
|
|
128
|
+
console.log('--- Test 3: Record Access (Simulate Recall) ---');
|
|
129
|
+
const weakObsId = 'obs-3';
|
|
130
|
+
console.log(`Recording access for ${weakObsId}...`);
|
|
131
|
+
const beforeAccess = scores.find(s => s.observationId === weakObsId);
|
|
132
|
+
console.log(`Before: activation=${beforeAccess?.activation.toFixed(4)}, strength=${beforeAccess?.strength.toFixed(2)}`);
|
|
133
|
+
await activationService.recordAccess(weakObsId);
|
|
134
|
+
const afterScores = await activationService.calculateActivationScores(entityId);
|
|
135
|
+
const afterAccess = afterScores.find(s => s.observationId === weakObsId);
|
|
136
|
+
console.log(`After: activation=${afterAccess?.activation.toFixed(4)}, strength=${afterAccess?.strength.toFixed(2)}`);
|
|
137
|
+
console.log(`✓ Strength increased from ${beforeAccess?.strength.toFixed(2)} to ${afterAccess?.strength.toFixed(2)}`);
|
|
138
|
+
console.log();
|
|
139
|
+
// Test 4: Prune weak memories (dry run)
|
|
140
|
+
console.log('--- Test 4: Prune Weak Memories (Dry Run) ---');
|
|
141
|
+
const pruneResult = await activationService.pruneWeakMemories(true, entityId);
|
|
142
|
+
console.log(`Candidates for deletion: ${pruneResult.candidates.length}`);
|
|
143
|
+
for (const candidate of pruneResult.candidates) {
|
|
144
|
+
console.log(` ${candidate.observationId}: ${candidate.reason}`);
|
|
145
|
+
}
|
|
146
|
+
console.log(`Would preserve: ${pruneResult.preserved} observations`);
|
|
147
|
+
console.log();
|
|
148
|
+
// Test 5: Activation decay over time
|
|
149
|
+
console.log('--- Test 5: Activation Decay Simulation ---');
|
|
150
|
+
console.log('Simulating activation decay for obs-2 over time:');
|
|
151
|
+
const testObs = scores.find(s => s.observationId === 'obs-2');
|
|
152
|
+
if (testObs) {
|
|
153
|
+
const strength = testObs.strength;
|
|
154
|
+
const timePoints = [0, 1, 3, 7, 14, 30, 60, 90];
|
|
155
|
+
console.log(`Strength: ${strength.toFixed(2)}`);
|
|
156
|
+
console.log('Time (days) | Activation | Retained?');
|
|
157
|
+
console.log('------------|------------|----------');
|
|
158
|
+
for (const days of timePoints) {
|
|
159
|
+
const activation = Math.exp(-days / strength);
|
|
160
|
+
const retained = activation >= 0.1;
|
|
161
|
+
console.log(`${days.toString().padStart(11)} | ${activation.toFixed(4).padStart(10)} | ${retained ? 'Yes' : 'No'}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
console.log();
|
|
165
|
+
// Test 6: Strength progression with repeated recalls
|
|
166
|
+
console.log('--- Test 6: Strength Progression with Repeated Recalls ---');
|
|
167
|
+
console.log('Simulating repeated recalls for a new observation:');
|
|
168
|
+
console.log('Recalls | Strength | Activation (7 days) | Retained?');
|
|
169
|
+
console.log('--------|----------|---------------------|----------');
|
|
170
|
+
for (let recalls = 0; recalls <= 10; recalls++) {
|
|
171
|
+
const strength = 1.0 + recalls * 1.0; // initialStrength + recalls * strengthIncrement
|
|
172
|
+
const activation = Math.exp(-7 / strength); // 7 days later
|
|
173
|
+
const retained = activation >= 0.1;
|
|
174
|
+
console.log(`${recalls.toString().padStart(7)} | ${strength.toFixed(2).padStart(8)} | ${activation.toFixed(4).padStart(19)} | ${retained ? 'Yes' : 'No'}`);
|
|
175
|
+
}
|
|
176
|
+
console.log();
|
|
177
|
+
// Test 7: Compare with different threshold values
|
|
178
|
+
console.log('--- Test 7: Impact of Different Retention Thresholds ---');
|
|
179
|
+
const thresholds = [0.05, 0.1, 0.2, 0.3];
|
|
180
|
+
console.log('Threshold | Retained | Deleted');
|
|
181
|
+
console.log('----------|----------|--------');
|
|
182
|
+
for (const threshold of thresholds) {
|
|
183
|
+
const testService = new memory_activation_1.MemoryActivationService(db, {
|
|
184
|
+
retentionThreshold: threshold,
|
|
185
|
+
initialStrength: 1.0,
|
|
186
|
+
strengthIncrement: 1.0,
|
|
187
|
+
timeUnit: 'days'
|
|
188
|
+
});
|
|
189
|
+
const testScores = await testService.calculateActivationScores(entityId);
|
|
190
|
+
const retained = testScores.filter(s => s.shouldRetain).length;
|
|
191
|
+
const deleted = testScores.filter(s => !s.shouldRetain).length;
|
|
192
|
+
console.log(`${threshold.toFixed(2).padStart(9)} | ${retained.toString().padStart(8)} | ${deleted.toString().padStart(7)}`);
|
|
193
|
+
}
|
|
194
|
+
console.log();
|
|
195
|
+
// Test 8: Verify Ebbinghaus curve shape
|
|
196
|
+
console.log('--- Test 8: Ebbinghaus Forgetting Curve Verification ---');
|
|
197
|
+
console.log('Classic Ebbinghaus curve: steep initial decline, then gradual leveling');
|
|
198
|
+
console.log('Time (days) | Retention (S=1) | Retention (S=5) | Retention (S=10)');
|
|
199
|
+
console.log('------------|-----------------|-----------------|------------------');
|
|
200
|
+
const curveTimePoints = [0, 0.5, 1, 2, 5, 10, 20, 30];
|
|
201
|
+
for (const days of curveTimePoints) {
|
|
202
|
+
const r1 = Math.exp(-days / 1);
|
|
203
|
+
const r5 = Math.exp(-days / 5);
|
|
204
|
+
const r10 = Math.exp(-days / 10);
|
|
205
|
+
console.log(`${days.toString().padStart(11)} | ${r1.toFixed(4).padStart(15)} | ${r5.toFixed(4).padStart(15)} | ${r10.toFixed(4).padStart(16)}`);
|
|
206
|
+
}
|
|
207
|
+
console.log();
|
|
208
|
+
console.log('✓ All memory activation tests completed successfully!');
|
|
209
|
+
console.log('\nKey Insights:');
|
|
210
|
+
console.log('- Higher strength (S) = slower forgetting');
|
|
211
|
+
console.log('- Each recall increases strength, making memories more durable');
|
|
212
|
+
console.log('- Activation threshold determines retention policy');
|
|
213
|
+
console.log('- System naturally implements spaced repetition via activation decay');
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error('Test failed:', error);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
db.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
testMemoryActivation();
|