cozo-memory 1.1.6 → 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.
- package/dist/memory-activation.js +295 -0
- package/dist/query-aware-traversal.js +291 -0
- package/dist/query-pipeline.js +355 -0
- package/dist/spreading-activation.js +308 -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/dist/test-spreading-activation.js +258 -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,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;
|