cozo-memory 1.1.7 → 1.1.8
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.
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SpreadingActivationService = void 0;
|
|
4
|
+
class SpreadingActivationService {
|
|
5
|
+
db;
|
|
6
|
+
embeddings;
|
|
7
|
+
config;
|
|
8
|
+
constructor(db, embeddings, config = {}) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.embeddings = embeddings;
|
|
11
|
+
this.config = {
|
|
12
|
+
spreadingFactor: config.spreadingFactor ?? 0.8,
|
|
13
|
+
decayFactor: config.decayFactor ?? 0.5,
|
|
14
|
+
temporalDecay: config.temporalDecay ?? 0.01,
|
|
15
|
+
inhibitionBeta: config.inhibitionBeta ?? 0.15,
|
|
16
|
+
inhibitionTopM: config.inhibitionTopM ?? 7,
|
|
17
|
+
propagationSteps: config.propagationSteps ?? 3,
|
|
18
|
+
activationThreshold: config.activationThreshold ?? 0.01,
|
|
19
|
+
sigmoidGamma: config.sigmoidGamma ?? 5.0,
|
|
20
|
+
sigmoidTheta: config.sigmoidTheta ?? 0.5,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Perform spreading activation from seed nodes
|
|
25
|
+
*/
|
|
26
|
+
async spreadActivation(query, seedTopK = 5) {
|
|
27
|
+
try {
|
|
28
|
+
// Step 1: Initialize - Find seed nodes via dual trigger (BM25 + Semantic)
|
|
29
|
+
const queryEmbedding = await this.embeddings.embed(query);
|
|
30
|
+
const seedNodes = await this.findSeedNodes(query, queryEmbedding, seedTopK);
|
|
31
|
+
if (seedNodes.length === 0) {
|
|
32
|
+
console.error('[SpreadingActivation] No seed nodes found');
|
|
33
|
+
return {
|
|
34
|
+
scores: [],
|
|
35
|
+
iterations: 0,
|
|
36
|
+
converged: false,
|
|
37
|
+
seedNodes: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Step 2: Initialize activation vector
|
|
41
|
+
let activation = new Map();
|
|
42
|
+
for (const seed of seedNodes) {
|
|
43
|
+
activation.set(seed.id, seed.score);
|
|
44
|
+
}
|
|
45
|
+
// Step 3: Propagate activation for T steps
|
|
46
|
+
let converged = false;
|
|
47
|
+
let iteration = 0;
|
|
48
|
+
for (iteration = 0; iteration < this.config.propagationSteps; iteration++) {
|
|
49
|
+
const newActivation = await this.propagateStep(activation);
|
|
50
|
+
// Check convergence (activation change < threshold)
|
|
51
|
+
const maxChange = this.calculateMaxChange(activation, newActivation);
|
|
52
|
+
if (maxChange < 0.001) {
|
|
53
|
+
converged = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
activation = newActivation;
|
|
57
|
+
}
|
|
58
|
+
// Step 4: Convert to result format
|
|
59
|
+
const scores = [];
|
|
60
|
+
for (const [entityId, act] of activation.entries()) {
|
|
61
|
+
if (act >= this.config.activationThreshold) {
|
|
62
|
+
const isSeed = seedNodes.some(s => s.id === entityId);
|
|
63
|
+
scores.push({
|
|
64
|
+
entityId,
|
|
65
|
+
activation: act,
|
|
66
|
+
potential: act, // After sigmoid, potential ≈ activation
|
|
67
|
+
source: isSeed ? 'seed' : 'propagated',
|
|
68
|
+
hops: isSeed ? 0 : iteration,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Sort by activation (highest first)
|
|
73
|
+
scores.sort((a, b) => b.activation - a.activation);
|
|
74
|
+
return {
|
|
75
|
+
scores,
|
|
76
|
+
iterations: iteration + 1,
|
|
77
|
+
converged,
|
|
78
|
+
seedNodes: seedNodes.map(s => s.id),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error('[SpreadingActivation] Error in spreadActivation:', error);
|
|
83
|
+
return {
|
|
84
|
+
scores: [],
|
|
85
|
+
iterations: 0,
|
|
86
|
+
converged: false,
|
|
87
|
+
seedNodes: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Find seed nodes using dual trigger: BM25 (lexical) + Semantic (dense)
|
|
93
|
+
*/
|
|
94
|
+
async findSeedNodes(query, queryEmbedding, topK) {
|
|
95
|
+
try {
|
|
96
|
+
// Get all entities
|
|
97
|
+
const allEntities = await this.db.run(`
|
|
98
|
+
?[id, name, embedding] :=
|
|
99
|
+
*entity{id, name, embedding}
|
|
100
|
+
`);
|
|
101
|
+
const scores = new Map();
|
|
102
|
+
// Calculate semantic similarity for each entity
|
|
103
|
+
for (const row of allEntities.rows) {
|
|
104
|
+
const [id, name, embedding] = row;
|
|
105
|
+
const entityEmbedding = embedding;
|
|
106
|
+
// Lexical score: simple keyword matching
|
|
107
|
+
const nameLower = name.toLowerCase();
|
|
108
|
+
const queryLower = query.toLowerCase();
|
|
109
|
+
const lexicalScore = nameLower.includes(queryLower) || queryLower.includes(nameLower) ? 1.0 : 0.0;
|
|
110
|
+
// Semantic score: cosine similarity
|
|
111
|
+
const semanticScore = this.cosineSimilarity(queryEmbedding, entityEmbedding);
|
|
112
|
+
// Combine scores (max of lexical and semantic)
|
|
113
|
+
const combinedScore = Math.max(lexicalScore, semanticScore);
|
|
114
|
+
scores.set(id, combinedScore);
|
|
115
|
+
}
|
|
116
|
+
// Sort and return top-K
|
|
117
|
+
return Array.from(scores.entries())
|
|
118
|
+
.map(([id, score]) => ({ id, score }))
|
|
119
|
+
.sort((a, b) => b.score - a.score)
|
|
120
|
+
.slice(0, topK);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error('[SpreadingActivation] Error finding seed nodes:', error);
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Calculate cosine similarity between two vectors
|
|
129
|
+
*/
|
|
130
|
+
cosineSimilarity(a, b) {
|
|
131
|
+
if (a.length !== b.length)
|
|
132
|
+
return 0;
|
|
133
|
+
let dotProduct = 0;
|
|
134
|
+
let normA = 0;
|
|
135
|
+
let normB = 0;
|
|
136
|
+
for (let i = 0; i < a.length; i++) {
|
|
137
|
+
dotProduct += a[i] * b[i];
|
|
138
|
+
normA += a[i] * a[i];
|
|
139
|
+
normB += b[i] * b[i];
|
|
140
|
+
}
|
|
141
|
+
if (normA === 0 || normB === 0)
|
|
142
|
+
return 0;
|
|
143
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Single propagation step with Fan Effect, Lateral Inhibition, and Sigmoid
|
|
147
|
+
*/
|
|
148
|
+
async propagateStep(currentActivation) {
|
|
149
|
+
try {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const newPotential = new Map();
|
|
152
|
+
// Step 1: Propagation with Fan Effect
|
|
153
|
+
// For each active node, spread activation to neighbors
|
|
154
|
+
for (const [nodeId, activation] of currentActivation.entries()) {
|
|
155
|
+
if (activation < this.config.activationThreshold)
|
|
156
|
+
continue;
|
|
157
|
+
// Get outgoing relationships
|
|
158
|
+
const relationships = await this.db.run(`
|
|
159
|
+
?[from_id, to_id, strength, created_at] :=
|
|
160
|
+
*relationship{from_id, to_id, strength, created_at},
|
|
161
|
+
from_id == $node_id
|
|
162
|
+
`, { node_id: nodeId });
|
|
163
|
+
// Calculate fan (out-degree)
|
|
164
|
+
const fan = Math.max(1, relationships.rows.length);
|
|
165
|
+
// Propagate to each neighbor
|
|
166
|
+
for (const row of relationships.rows) {
|
|
167
|
+
const [, toId, strength, createdAt] = row;
|
|
168
|
+
const targetId = toId;
|
|
169
|
+
// Calculate edge weight with temporal decay
|
|
170
|
+
const timeDiff = (now - createdAt) / (1000 * 60 * 60 * 24); // days
|
|
171
|
+
const temporalWeight = Math.exp(-this.config.temporalDecay * timeDiff);
|
|
172
|
+
const edgeWeight = strength * temporalWeight;
|
|
173
|
+
// Spread activation with fan effect
|
|
174
|
+
const spreadAmount = (this.config.spreadingFactor * edgeWeight * activation) / fan;
|
|
175
|
+
// Accumulate potential
|
|
176
|
+
const currentPotential = newPotential.get(targetId) || 0;
|
|
177
|
+
newPotential.set(targetId, currentPotential + spreadAmount);
|
|
178
|
+
}
|
|
179
|
+
// Node decay: retain some of current activation
|
|
180
|
+
const retainedActivation = (1 - this.config.decayFactor) * activation;
|
|
181
|
+
const currentPotential = newPotential.get(nodeId) || 0;
|
|
182
|
+
newPotential.set(nodeId, currentPotential + retainedActivation);
|
|
183
|
+
}
|
|
184
|
+
// Step 2: Lateral Inhibition
|
|
185
|
+
// Top-M nodes inhibit weaker competitors
|
|
186
|
+
const potentialArray = Array.from(newPotential.entries())
|
|
187
|
+
.sort((a, b) => b[1] - a[1]);
|
|
188
|
+
const topM = potentialArray.slice(0, this.config.inhibitionTopM);
|
|
189
|
+
const inhibitedPotential = new Map();
|
|
190
|
+
for (const [nodeId, potential] of newPotential.entries()) {
|
|
191
|
+
let inhibition = 0;
|
|
192
|
+
// Calculate inhibition from top-M nodes
|
|
193
|
+
for (const [topId, topPotential] of topM) {
|
|
194
|
+
if (topId !== nodeId && topPotential > potential) {
|
|
195
|
+
inhibition += this.config.inhibitionBeta * (topPotential - potential);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Apply inhibition (cannot go below 0)
|
|
199
|
+
const inhibitedValue = Math.max(0, potential - inhibition);
|
|
200
|
+
inhibitedPotential.set(nodeId, inhibitedValue);
|
|
201
|
+
}
|
|
202
|
+
// Step 3: Sigmoid Activation Function
|
|
203
|
+
const newActivation = new Map();
|
|
204
|
+
for (const [nodeId, potential] of inhibitedPotential.entries()) {
|
|
205
|
+
const activation = this.sigmoid(potential);
|
|
206
|
+
if (activation >= this.config.activationThreshold) {
|
|
207
|
+
newActivation.set(nodeId, activation);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return newActivation;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.error('[SpreadingActivation] Error in propagateStep:', error);
|
|
214
|
+
return currentActivation;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Sigmoid activation function: σ(u) = 1 / (1 + exp(-γ(u - θ)))
|
|
219
|
+
*/
|
|
220
|
+
sigmoid(potential) {
|
|
221
|
+
const exponent = -this.config.sigmoidGamma * (potential - this.config.sigmoidTheta);
|
|
222
|
+
return 1 / (1 + Math.exp(exponent));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Calculate maximum activation change between iterations
|
|
226
|
+
*/
|
|
227
|
+
calculateMaxChange(oldActivation, newActivation) {
|
|
228
|
+
let maxChange = 0;
|
|
229
|
+
// Check all nodes in both maps
|
|
230
|
+
const allNodes = new Set([...oldActivation.keys(), ...newActivation.keys()]);
|
|
231
|
+
for (const nodeId of allNodes) {
|
|
232
|
+
const oldValue = oldActivation.get(nodeId) || 0;
|
|
233
|
+
const newValue = newActivation.get(nodeId) || 0;
|
|
234
|
+
const change = Math.abs(newValue - oldValue);
|
|
235
|
+
maxChange = Math.max(maxChange, change);
|
|
236
|
+
}
|
|
237
|
+
return maxChange;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Triple Hybrid Retrieval: Semantic + Activation + PageRank
|
|
241
|
+
*/
|
|
242
|
+
async tripleHybridRetrieval(query, options = {}) {
|
|
243
|
+
try {
|
|
244
|
+
const { topK = 30, lambdaSemantic = 0.5, lambdaActivation = 0.3, lambdaStructural = 0.2, seedTopK = 5, } = options;
|
|
245
|
+
// 1. Semantic similarity
|
|
246
|
+
const queryEmbedding = await this.embeddings.embed(query);
|
|
247
|
+
const allEntities = await this.db.run(`
|
|
248
|
+
?[id, embedding] :=
|
|
249
|
+
*entity{id, embedding}
|
|
250
|
+
`);
|
|
251
|
+
const semanticScores = new Map();
|
|
252
|
+
for (const row of allEntities.rows) {
|
|
253
|
+
const [id, embedding] = row;
|
|
254
|
+
const score = this.cosineSimilarity(queryEmbedding, embedding);
|
|
255
|
+
semanticScores.set(id, score);
|
|
256
|
+
}
|
|
257
|
+
// 2. Spreading activation
|
|
258
|
+
const activationResult = await this.spreadActivation(query, seedTopK);
|
|
259
|
+
const activationScores = new Map();
|
|
260
|
+
for (const score of activationResult.scores) {
|
|
261
|
+
activationScores.set(score.entityId, score.activation);
|
|
262
|
+
}
|
|
263
|
+
// 3. PageRank (structural importance)
|
|
264
|
+
const pageRankResults = await this.db.run(`
|
|
265
|
+
?[id, rank] :=
|
|
266
|
+
*entity_rank{entity_id: id, rank}
|
|
267
|
+
`);
|
|
268
|
+
const pageRankScores = new Map();
|
|
269
|
+
for (const row of pageRankResults.rows) {
|
|
270
|
+
const [id, rank] = row;
|
|
271
|
+
pageRankScores.set(id, rank);
|
|
272
|
+
}
|
|
273
|
+
// 4. Combine scores
|
|
274
|
+
const allEntityIds = new Set([
|
|
275
|
+
...semanticScores.keys(),
|
|
276
|
+
...activationScores.keys(),
|
|
277
|
+
...pageRankScores.keys(),
|
|
278
|
+
]);
|
|
279
|
+
const results = [];
|
|
280
|
+
for (const entityId of allEntityIds) {
|
|
281
|
+
const semantic = semanticScores.get(entityId) || 0;
|
|
282
|
+
const activation = activationScores.get(entityId) || 0;
|
|
283
|
+
const structural = pageRankScores.get(entityId) || 0;
|
|
284
|
+
const combinedScore = lambdaSemantic * semantic +
|
|
285
|
+
lambdaActivation * activation +
|
|
286
|
+
lambdaStructural * structural;
|
|
287
|
+
results.push({
|
|
288
|
+
entityId,
|
|
289
|
+
score: combinedScore,
|
|
290
|
+
breakdown: {
|
|
291
|
+
semantic,
|
|
292
|
+
activation,
|
|
293
|
+
structural,
|
|
294
|
+
formula: `${lambdaSemantic}×${semantic.toFixed(3)} + ${lambdaActivation}×${activation.toFixed(3)} + ${lambdaStructural}×${structural.toFixed(3)}`,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// Sort by combined score and return top-K
|
|
299
|
+
results.sort((a, b) => b.score - a.score);
|
|
300
|
+
return results.slice(0, topK);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
console.error('[SpreadingActivation] Error in tripleHybridRetrieval:', error);
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
exports.SpreadingActivationService = SpreadingActivationService;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const cozo_node_1 = require("cozo-node");
|
|
4
|
+
const spreading_activation_1 = require("./spreading-activation");
|
|
5
|
+
const embedding_service_1 = require("./embedding-service");
|
|
6
|
+
async function testSpreadingActivation() {
|
|
7
|
+
console.log('=== SYNAPSE Spreading Activation Test ===\n');
|
|
8
|
+
const db = new cozo_node_1.CozoDb();
|
|
9
|
+
const embeddingService = new embedding_service_1.EmbeddingService();
|
|
10
|
+
const synapseService = new spreading_activation_1.SpreadingActivationService(db, embeddingService, {
|
|
11
|
+
spreadingFactor: 0.8,
|
|
12
|
+
decayFactor: 0.5,
|
|
13
|
+
temporalDecay: 0.01,
|
|
14
|
+
inhibitionBeta: 0.15,
|
|
15
|
+
inhibitionTopM: 7,
|
|
16
|
+
propagationSteps: 3,
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
// Setup: Create test graph
|
|
20
|
+
console.log('--- Setup: Creating test knowledge graph ---');
|
|
21
|
+
// Create entity relation
|
|
22
|
+
await db.run(`
|
|
23
|
+
:create entity {
|
|
24
|
+
id: String,
|
|
25
|
+
name: String,
|
|
26
|
+
type: String,
|
|
27
|
+
=>
|
|
28
|
+
embedding: <F32; 1024>,
|
|
29
|
+
metadata: Any
|
|
30
|
+
}
|
|
31
|
+
`);
|
|
32
|
+
// Create relationship relation
|
|
33
|
+
await db.run(`
|
|
34
|
+
:create relationship {
|
|
35
|
+
from_id: String,
|
|
36
|
+
to_id: String,
|
|
37
|
+
relation_type: String,
|
|
38
|
+
=>
|
|
39
|
+
strength: Float,
|
|
40
|
+
created_at: Int,
|
|
41
|
+
metadata: Any
|
|
42
|
+
}
|
|
43
|
+
`);
|
|
44
|
+
// Create entity_rank relation for PageRank
|
|
45
|
+
await db.run(`
|
|
46
|
+
:create entity_rank {
|
|
47
|
+
entity_id: String,
|
|
48
|
+
=>
|
|
49
|
+
rank: Float
|
|
50
|
+
}
|
|
51
|
+
`);
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const oneDayAgo = now - (24 * 60 * 60 * 1000);
|
|
54
|
+
const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000);
|
|
55
|
+
// Create entities
|
|
56
|
+
const entities = [
|
|
57
|
+
{ id: 'e1', name: 'TypeScript', type: 'Technology' },
|
|
58
|
+
{ id: 'e2', name: 'JavaScript', type: 'Technology' },
|
|
59
|
+
{ id: 'e3', name: 'React', type: 'Framework' },
|
|
60
|
+
{ id: 'e4', name: 'Node.js', type: 'Runtime' },
|
|
61
|
+
{ id: 'e5', name: 'Alice', type: 'Person' },
|
|
62
|
+
{ id: 'e6', name: 'Bob', type: 'Person' },
|
|
63
|
+
{ id: 'e7', name: 'Frontend Development', type: 'Concept' },
|
|
64
|
+
{ id: 'e8', name: 'Backend Development', type: 'Concept' },
|
|
65
|
+
];
|
|
66
|
+
for (const entity of entities) {
|
|
67
|
+
const embedding = await embeddingService.embed(entity.name);
|
|
68
|
+
await db.run(`
|
|
69
|
+
?[id, name, type, embedding, metadata] <- [
|
|
70
|
+
[$id, $name, $type, $embedding, {}]
|
|
71
|
+
]
|
|
72
|
+
:put entity {id, name, type => embedding, metadata}
|
|
73
|
+
`, {
|
|
74
|
+
id: entity.id,
|
|
75
|
+
name: entity.name,
|
|
76
|
+
type: entity.type,
|
|
77
|
+
embedding,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Create relationships (with temporal and strength variations)
|
|
81
|
+
const relationships = [
|
|
82
|
+
// TypeScript ecosystem
|
|
83
|
+
{ from: 'e1', to: 'e2', type: 'superset_of', strength: 0.9, time: now },
|
|
84
|
+
{ from: 'e1', to: 'e3', type: 'used_with', strength: 0.8, time: oneDayAgo },
|
|
85
|
+
{ from: 'e1', to: 'e4', type: 'runs_on', strength: 0.7, time: oneDayAgo },
|
|
86
|
+
// React connections
|
|
87
|
+
{ from: 'e3', to: 'e2', type: 'built_with', strength: 0.9, time: now },
|
|
88
|
+
{ from: 'e3', to: 'e7', type: 'part_of', strength: 0.8, time: now },
|
|
89
|
+
// Node.js connections
|
|
90
|
+
{ from: 'e4', to: 'e2', type: 'executes', strength: 0.9, time: now },
|
|
91
|
+
{ from: 'e4', to: 'e8', type: 'part_of', strength: 0.8, time: now },
|
|
92
|
+
// People connections
|
|
93
|
+
{ from: 'e5', to: 'e1', type: 'expert_in', strength: 0.9, time: now },
|
|
94
|
+
{ from: 'e5', to: 'e3', type: 'uses', strength: 0.7, time: oneDayAgo },
|
|
95
|
+
{ from: 'e6', to: 'e4', type: 'expert_in', strength: 0.9, time: now },
|
|
96
|
+
{ from: 'e6', to: 'e8', type: 'works_on', strength: 0.8, time: now },
|
|
97
|
+
// Weak/old connection (should be dampened by temporal decay)
|
|
98
|
+
{ from: 'e7', to: 'e8', type: 'related_to', strength: 0.3, time: oneWeekAgo },
|
|
99
|
+
];
|
|
100
|
+
for (const rel of relationships) {
|
|
101
|
+
await db.run(`
|
|
102
|
+
?[from_id, to_id, relation_type, strength, created_at, metadata] <- [
|
|
103
|
+
[$from_id, $to_id, $relation_type, $strength, $created_at, {}]
|
|
104
|
+
]
|
|
105
|
+
:put relationship {from_id, to_id, relation_type => strength, created_at, metadata}
|
|
106
|
+
`, {
|
|
107
|
+
from_id: rel.from,
|
|
108
|
+
to_id: rel.to,
|
|
109
|
+
relation_type: rel.type,
|
|
110
|
+
strength: rel.strength,
|
|
111
|
+
created_at: rel.time,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Create PageRank scores (simulated)
|
|
115
|
+
const pageRanks = [
|
|
116
|
+
{ id: 'e1', rank: 0.25 }, // TypeScript - high importance
|
|
117
|
+
{ id: 'e2', rank: 0.20 }, // JavaScript - high importance
|
|
118
|
+
{ id: 'e3', rank: 0.15 }, // React
|
|
119
|
+
{ id: 'e4', rank: 0.15 }, // Node.js
|
|
120
|
+
{ id: 'e5', rank: 0.10 }, // Alice
|
|
121
|
+
{ id: 'e6', rank: 0.08 }, // Bob
|
|
122
|
+
{ id: 'e7', rank: 0.04 }, // Frontend
|
|
123
|
+
{ id: 'e8', rank: 0.03 }, // Backend
|
|
124
|
+
];
|
|
125
|
+
for (const pr of pageRanks) {
|
|
126
|
+
await db.run(`
|
|
127
|
+
?[entity_id, rank] <- [[$entity_id, $rank]]
|
|
128
|
+
:put entity_rank {entity_id => rank}
|
|
129
|
+
`, { entity_id: pr.id, rank: pr.rank });
|
|
130
|
+
}
|
|
131
|
+
console.log('✓ Created 8 entities and 12 relationships\n');
|
|
132
|
+
// Test 1: Basic Spreading Activation
|
|
133
|
+
console.log('--- Test 1: Basic Spreading Activation ---');
|
|
134
|
+
console.log('Query: "TypeScript programming"');
|
|
135
|
+
const result1 = await synapseService.spreadActivation('TypeScript programming', 3);
|
|
136
|
+
console.log(`Iterations: ${result1.iterations}, Converged: ${result1.converged}`);
|
|
137
|
+
console.log(`Seed nodes: ${result1.seedNodes.join(', ')}`);
|
|
138
|
+
console.log('\nActivation Scores (Top 5):');
|
|
139
|
+
for (const score of result1.scores.slice(0, 5)) {
|
|
140
|
+
const entity = entities.find(e => e.id === score.entityId);
|
|
141
|
+
console.log(` ${entity?.name} (${score.entityId}): ${score.activation.toFixed(4)} [${score.source}, ${score.hops} hops]`);
|
|
142
|
+
}
|
|
143
|
+
console.log();
|
|
144
|
+
// Test 2: Multi-Hop Reasoning
|
|
145
|
+
console.log('--- Test 2: Multi-Hop Reasoning (Bridge Node Effect) ---');
|
|
146
|
+
console.log('Query: "Frontend expert" (should find Alice via TypeScript → React → Frontend)');
|
|
147
|
+
const result2 = await synapseService.spreadActivation('Frontend expert', 2);
|
|
148
|
+
console.log('\nActivation Scores (All activated nodes):');
|
|
149
|
+
for (const score of result2.scores) {
|
|
150
|
+
const entity = entities.find(e => e.id === score.entityId);
|
|
151
|
+
console.log(` ${entity?.name}: ${score.activation.toFixed(4)} [${score.source}]`);
|
|
152
|
+
}
|
|
153
|
+
console.log();
|
|
154
|
+
// Test 3: Temporal Decay Effect
|
|
155
|
+
console.log('--- Test 3: Temporal Decay Effect ---');
|
|
156
|
+
console.log('Recent connections should have higher activation than old ones');
|
|
157
|
+
// Create two similar relationships with different timestamps
|
|
158
|
+
const recentTime = now;
|
|
159
|
+
const oldTime = now - (30 * 24 * 60 * 60 * 1000); // 30 days ago
|
|
160
|
+
await db.run(`
|
|
161
|
+
?[from_id, to_id, relation_type, strength, created_at, metadata] <- [
|
|
162
|
+
['e_test_recent', 'e_test_target', 'test_rel', 0.8, $recent_time, {}],
|
|
163
|
+
['e_test_old', 'e_test_target', 'test_rel', 0.8, $old_time, {}]
|
|
164
|
+
]
|
|
165
|
+
:put relationship {from_id, to_id, relation_type => strength, created_at, metadata}
|
|
166
|
+
`, { recent_time: recentTime, old_time: oldTime });
|
|
167
|
+
console.log('✓ Created test relationships with different timestamps');
|
|
168
|
+
console.log(' Recent: strength=0.8, age=0 days');
|
|
169
|
+
console.log(' Old: strength=0.8, age=30 days');
|
|
170
|
+
console.log(' Expected: Recent should propagate more activation due to temporal decay\n');
|
|
171
|
+
// Test 4: Lateral Inhibition
|
|
172
|
+
console.log('--- Test 4: Lateral Inhibition ---');
|
|
173
|
+
console.log('High-activation nodes should suppress weaker competitors');
|
|
174
|
+
const result4 = await synapseService.spreadActivation('JavaScript TypeScript React', 5);
|
|
175
|
+
console.log('\nTop-M nodes (should inhibit others):');
|
|
176
|
+
const topM = result4.scores.slice(0, 7);
|
|
177
|
+
for (const score of topM) {
|
|
178
|
+
const entity = entities.find(e => e.id === score.entityId);
|
|
179
|
+
console.log(` ${entity?.name}: ${score.activation.toFixed(4)}`);
|
|
180
|
+
}
|
|
181
|
+
console.log('\nSuppressed nodes (below threshold):');
|
|
182
|
+
const suppressed = result4.scores.slice(7);
|
|
183
|
+
for (const score of suppressed) {
|
|
184
|
+
const entity = entities.find(e => e.id === score.entityId);
|
|
185
|
+
console.log(` ${entity?.name}: ${score.activation.toFixed(4)} (suppressed by inhibition)`);
|
|
186
|
+
}
|
|
187
|
+
console.log();
|
|
188
|
+
// Test 5: Triple Hybrid Retrieval
|
|
189
|
+
console.log('--- Test 5: Triple Hybrid Retrieval ---');
|
|
190
|
+
console.log('Query: "TypeScript development"');
|
|
191
|
+
console.log('Combining: Semantic (50%) + Activation (30%) + PageRank (20%)');
|
|
192
|
+
const result5 = await synapseService.tripleHybridRetrieval('TypeScript development', {
|
|
193
|
+
topK: 5,
|
|
194
|
+
lambdaSemantic: 0.5,
|
|
195
|
+
lambdaActivation: 0.3,
|
|
196
|
+
lambdaStructural: 0.2,
|
|
197
|
+
seedTopK: 3,
|
|
198
|
+
});
|
|
199
|
+
console.log('\nHybrid Scores (Top 5):');
|
|
200
|
+
for (const result of result5) {
|
|
201
|
+
const entity = entities.find(e => e.id === result.entityId);
|
|
202
|
+
console.log(` ${entity?.name}:`);
|
|
203
|
+
console.log(` Combined: ${result.score.toFixed(4)}`);
|
|
204
|
+
console.log(` Breakdown: ${result.breakdown.formula}`);
|
|
205
|
+
console.log(` Semantic: ${result.breakdown.semantic.toFixed(4)}`);
|
|
206
|
+
console.log(` Activation: ${result.breakdown.activation.toFixed(4)}`);
|
|
207
|
+
console.log(` Structural: ${result.breakdown.structural.toFixed(4)}`);
|
|
208
|
+
}
|
|
209
|
+
console.log();
|
|
210
|
+
// Test 6: Fan Effect
|
|
211
|
+
console.log('--- Test 6: Fan Effect (Attention Dilution) ---');
|
|
212
|
+
console.log('Nodes with many outgoing edges should dilute their activation');
|
|
213
|
+
// JavaScript has many connections (high fan)
|
|
214
|
+
// Alice has few connections (low fan)
|
|
215
|
+
const jsConnections = relationships.filter(r => r.from === 'e2').length;
|
|
216
|
+
const aliceConnections = relationships.filter(r => r.from === 'e5').length;
|
|
217
|
+
console.log(`JavaScript (e2) out-degree: ${jsConnections}`);
|
|
218
|
+
console.log(`Alice (e5) out-degree: ${aliceConnections}`);
|
|
219
|
+
console.log('Expected: Alice should spread more activation per edge due to lower fan\n');
|
|
220
|
+
// Test 7: Convergence Analysis
|
|
221
|
+
console.log('--- Test 7: Convergence Analysis ---');
|
|
222
|
+
console.log('Testing activation convergence over iterations');
|
|
223
|
+
const convergenceResults = [];
|
|
224
|
+
for (let steps = 1; steps <= 5; steps++) {
|
|
225
|
+
const service = new spreading_activation_1.SpreadingActivationService(db, embeddingService, {
|
|
226
|
+
propagationSteps: steps,
|
|
227
|
+
});
|
|
228
|
+
const result = await service.spreadActivation('TypeScript', 2);
|
|
229
|
+
convergenceResults.push({
|
|
230
|
+
steps,
|
|
231
|
+
converged: result.converged,
|
|
232
|
+
iterations: result.iterations,
|
|
233
|
+
totalActivation: result.scores.reduce((sum, s) => sum + s.activation, 0),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
console.log('\nSteps | Converged | Iterations | Total Activation');
|
|
237
|
+
console.log('------|-----------|------------|------------------');
|
|
238
|
+
for (const res of convergenceResults) {
|
|
239
|
+
console.log(` ${res.steps} | ${res.converged ? 'Yes' : 'No '} | ${res.iterations} | ${res.totalActivation.toFixed(4)}`);
|
|
240
|
+
}
|
|
241
|
+
console.log();
|
|
242
|
+
console.log('✓ All SYNAPSE spreading activation tests completed successfully!\n');
|
|
243
|
+
console.log('Key Insights:');
|
|
244
|
+
console.log('- Spreading Activation propagates relevance through graph structure');
|
|
245
|
+
console.log('- Lateral Inhibition suppresses weak competitors, focusing on salient nodes');
|
|
246
|
+
console.log('- Fan Effect dilutes activation from high-degree nodes (attention distribution)');
|
|
247
|
+
console.log('- Temporal Decay prioritizes recent connections over old ones');
|
|
248
|
+
console.log('- Triple Hybrid combines geometric (semantic), dynamic (activation), and structural (PageRank) signals');
|
|
249
|
+
console.log('- System converges within 3 iterations, making it efficient for real-time retrieval');
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error('Test failed:', error);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
db.close();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
testSpreadingActivation().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozo-memory",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"mcpName": "io.github.tobs-code/cozo-memory",
|
|
5
5
|
"description": "Local-first persistent memory system for AI agents with hybrid search, graph reasoning, and MCP integration",
|
|
6
6
|
"main": "dist/index.js",
|