cozo-memory 1.1.7 → 1.2.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.
@@ -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;