@yugenlab/vaayu 0.1.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/LICENSE +21 -0
- package/README.md +365 -0
- package/chunks/chunk-E5A3SCDJ.js +246 -0
- package/chunks/chunk-G5VYCA6O.js +69 -0
- package/chunks/chunk-H76V36OF.js +1029 -0
- package/chunks/chunk-HAPVUJ6A.js +238 -0
- package/chunks/chunk-IEKAYVA3.js +137 -0
- package/chunks/chunk-IGKYKEKT.js +43 -0
- package/chunks/chunk-IIET2K6D.js +7728 -0
- package/chunks/chunk-ITIVYGUG.js +347 -0
- package/chunks/chunk-JAWZ7ANC.js +208 -0
- package/chunks/chunk-JZU37VQ5.js +714 -0
- package/chunks/chunk-KC6NRZ7U.js +198 -0
- package/chunks/chunk-KDRROLVN.js +433 -0
- package/chunks/chunk-L7JICQBW.js +1006 -0
- package/chunks/chunk-MINFB5LT.js +1479 -0
- package/chunks/chunk-MJ74G5RB.js +5816 -0
- package/chunks/chunk-S4TBVCL2.js +2158 -0
- package/chunks/chunk-SMVJRPAH.js +2753 -0
- package/chunks/chunk-U6OLJ36B.js +438 -0
- package/chunks/chunk-URGEODS5.js +752 -0
- package/chunks/chunk-YSU3BWV6.js +123 -0
- package/chunks/consolidation-indexer-TOTTDZXW.js +21 -0
- package/chunks/day-consolidation-NKO63HZQ.js +24 -0
- package/chunks/graphrag-ZI2FSU7S.js +13 -0
- package/chunks/hierarchical-temporal-search-ZD46UMKR.js +8 -0
- package/chunks/hybrid-search-ZVLZVGFS.js +19 -0
- package/chunks/memory-store-KNJPMBLQ.js +17 -0
- package/chunks/periodic-consolidation-BPKOZDGB.js +10 -0
- package/chunks/postgres-3ZXBYTPC.js +8 -0
- package/chunks/recall-GMVHWQWW.js +20 -0
- package/chunks/search-7HZETVMZ.js +18 -0
- package/chunks/session-store-XKPGKXUS.js +44 -0
- package/chunks/sqlite-JPF5TICX.js +152 -0
- package/chunks/src-6GVZTUH6.js +12 -0
- package/chunks/src-QAXOD5SB.js +273 -0
- package/chunks/suncalc-NOHGYHDU.js +186 -0
- package/chunks/tree-RSHKDTCR.js +10 -0
- package/gateway.js +61944 -0
- package/package.json +51 -0
- package/pair-cli.js +133 -0
|
@@ -0,0 +1,2158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALPHA,
|
|
3
|
+
BETA,
|
|
4
|
+
EmbeddingService,
|
|
5
|
+
GAMMA,
|
|
6
|
+
STOP_WORDS,
|
|
7
|
+
cosineSimilarity,
|
|
8
|
+
textMatchScore
|
|
9
|
+
} from "./chunk-JAWZ7ANC.js";
|
|
10
|
+
import {
|
|
11
|
+
DatabaseManager,
|
|
12
|
+
initGraphSchema
|
|
13
|
+
} from "./chunk-U6OLJ36B.js";
|
|
14
|
+
import {
|
|
15
|
+
getChitraguptaHome
|
|
16
|
+
} from "./chunk-KC6NRZ7U.js";
|
|
17
|
+
|
|
18
|
+
// ../chitragupta/packages/smriti/src/graphrag.ts
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
21
|
+
|
|
22
|
+
// ../chitragupta/packages/smriti/src/graphrag-pagerank.ts
|
|
23
|
+
var PAGERANK_DAMPING = 0.85;
|
|
24
|
+
var PAGERANK_EPSILON = 1e-4;
|
|
25
|
+
var PAGERANK_MAX_ITERATIONS = 100;
|
|
26
|
+
function computePageRank(graph) {
|
|
27
|
+
const N = graph.nodes.length;
|
|
28
|
+
if (N === 0) return /* @__PURE__ */ new Map();
|
|
29
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
30
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const node of graph.nodes) {
|
|
32
|
+
ranks.set(node.id, 1 / N);
|
|
33
|
+
nodeIds.add(node.id);
|
|
34
|
+
}
|
|
35
|
+
const outDegree = /* @__PURE__ */ new Map();
|
|
36
|
+
const inLinks = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const node of graph.nodes) {
|
|
38
|
+
outDegree.set(node.id, 0);
|
|
39
|
+
inLinks.set(node.id, []);
|
|
40
|
+
}
|
|
41
|
+
for (const edge of graph.edges) {
|
|
42
|
+
if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
|
|
43
|
+
outDegree.set(edge.source, (outDegree.get(edge.source) ?? 0) + 1);
|
|
44
|
+
inLinks.get(edge.target).push(edge.source);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const baseFactor = (1 - PAGERANK_DAMPING) / N;
|
|
48
|
+
for (let iteration = 0; iteration < PAGERANK_MAX_ITERATIONS; iteration++) {
|
|
49
|
+
const newRanks = /* @__PURE__ */ new Map();
|
|
50
|
+
let maxDelta = 0;
|
|
51
|
+
let danglingRank = 0;
|
|
52
|
+
for (const node of graph.nodes) {
|
|
53
|
+
if ((outDegree.get(node.id) ?? 0) === 0) {
|
|
54
|
+
danglingRank += ranks.get(node.id) ?? 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const node of graph.nodes) {
|
|
58
|
+
let incomingRank = 0;
|
|
59
|
+
const sources = inLinks.get(node.id) ?? [];
|
|
60
|
+
for (const source of sources) {
|
|
61
|
+
const sourceRank = ranks.get(source) ?? 0;
|
|
62
|
+
const sourceOut = outDegree.get(source) ?? 1;
|
|
63
|
+
incomingRank += sourceRank / sourceOut;
|
|
64
|
+
}
|
|
65
|
+
const rank = baseFactor + PAGERANK_DAMPING * (incomingRank + danglingRank / N);
|
|
66
|
+
newRanks.set(node.id, rank);
|
|
67
|
+
const delta = Math.abs(rank - (ranks.get(node.id) ?? 0));
|
|
68
|
+
if (delta > maxDelta) maxDelta = delta;
|
|
69
|
+
}
|
|
70
|
+
for (const [id, rank] of newRanks) {
|
|
71
|
+
ranks.set(id, rank);
|
|
72
|
+
}
|
|
73
|
+
if (maxDelta < PAGERANK_EPSILON) break;
|
|
74
|
+
}
|
|
75
|
+
return ranks;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ../chitragupta/packages/smriti/src/graphrag-pagerank-personalized.ts
|
|
79
|
+
function buildTopicBias(nodeIds, nodeContents, topic) {
|
|
80
|
+
const bias = /* @__PURE__ */ new Map();
|
|
81
|
+
const n = nodeIds.length;
|
|
82
|
+
if (!topic || n === 0) {
|
|
83
|
+
const uniform = 1 / Math.max(n, 1);
|
|
84
|
+
for (const id of nodeIds) bias.set(id, uniform);
|
|
85
|
+
return bias;
|
|
86
|
+
}
|
|
87
|
+
const queryTerms = tokenizeSimple(topic);
|
|
88
|
+
const queryTf = termFrequency(queryTerms);
|
|
89
|
+
let totalSim = 0;
|
|
90
|
+
const similarities = new Array(n);
|
|
91
|
+
for (let i = 0; i < n; i++) {
|
|
92
|
+
const content = nodeContents.get(nodeIds[i]) ?? "";
|
|
93
|
+
const docTerms = tokenizeSimple(content);
|
|
94
|
+
const docTf = termFrequency(docTerms);
|
|
95
|
+
const sim = tfCosineSimilarity(queryTf, docTf);
|
|
96
|
+
similarities[i] = sim;
|
|
97
|
+
totalSim += sim;
|
|
98
|
+
}
|
|
99
|
+
if (totalSim === 0) {
|
|
100
|
+
const uniform = 1 / n;
|
|
101
|
+
for (const id of nodeIds) bias.set(id, uniform);
|
|
102
|
+
return bias;
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < n; i++) {
|
|
105
|
+
bias.set(nodeIds[i], similarities[i] / totalSim);
|
|
106
|
+
}
|
|
107
|
+
return bias;
|
|
108
|
+
}
|
|
109
|
+
function tokenizeSimple(text) {
|
|
110
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 2);
|
|
111
|
+
}
|
|
112
|
+
function termFrequency(tokens) {
|
|
113
|
+
const tf = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const t of tokens) {
|
|
115
|
+
tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
116
|
+
}
|
|
117
|
+
return tf;
|
|
118
|
+
}
|
|
119
|
+
function tfCosineSimilarity(a, b) {
|
|
120
|
+
let dot = 0;
|
|
121
|
+
let normA = 0;
|
|
122
|
+
let normB = 0;
|
|
123
|
+
for (const [term, freqA] of a) {
|
|
124
|
+
normA += freqA * freqA;
|
|
125
|
+
const freqB = b.get(term);
|
|
126
|
+
if (freqB !== void 0) dot += freqA * freqB;
|
|
127
|
+
}
|
|
128
|
+
for (const freqB of b.values()) {
|
|
129
|
+
normB += freqB * freqB;
|
|
130
|
+
}
|
|
131
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
132
|
+
return denom === 0 ? 0 : dot / denom;
|
|
133
|
+
}
|
|
134
|
+
function computePersonalizedPageRank(graph, topicBias, opts) {
|
|
135
|
+
const damping = opts?.damping ?? 0.85;
|
|
136
|
+
const epsilon = opts?.epsilon ?? 1e-6;
|
|
137
|
+
const maxIter = opts?.maxIterations ?? 150;
|
|
138
|
+
const useGS = opts?.useGaussSeidel ?? true;
|
|
139
|
+
const N = graph.nodes.length;
|
|
140
|
+
if (N === 0) return /* @__PURE__ */ new Map();
|
|
141
|
+
const nodeIds = graph.nodes.map((n) => n.id);
|
|
142
|
+
const nodeIdSet = new Set(nodeIds);
|
|
143
|
+
const outDegree = /* @__PURE__ */ new Map();
|
|
144
|
+
const inLinks = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const id of nodeIds) {
|
|
146
|
+
outDegree.set(id, 0);
|
|
147
|
+
inLinks.set(id, []);
|
|
148
|
+
}
|
|
149
|
+
for (const edge of graph.edges) {
|
|
150
|
+
if (nodeIdSet.has(edge.source) && nodeIdSet.has(edge.target)) {
|
|
151
|
+
outDegree.set(edge.source, (outDegree.get(edge.source) ?? 0) + 1);
|
|
152
|
+
inLinks.get(edge.target).push(edge.source);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let bias;
|
|
156
|
+
if (typeof topicBias === "string") {
|
|
157
|
+
const nodeContents = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const node of graph.nodes) {
|
|
159
|
+
nodeContents.set(node.id, node.content);
|
|
160
|
+
}
|
|
161
|
+
bias = buildTopicBias(nodeIds, nodeContents, topicBias);
|
|
162
|
+
} else if (topicBias instanceof Map) {
|
|
163
|
+
bias = topicBias;
|
|
164
|
+
} else {
|
|
165
|
+
const uniform = 1 / N;
|
|
166
|
+
bias = new Map(nodeIds.map((id) => [id, uniform]));
|
|
167
|
+
}
|
|
168
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const id of nodeIds) {
|
|
170
|
+
ranks.set(id, 1 / N);
|
|
171
|
+
}
|
|
172
|
+
const danglingNodes = [];
|
|
173
|
+
for (const id of nodeIds) {
|
|
174
|
+
if ((outDegree.get(id) ?? 0) === 0) danglingNodes.push(id);
|
|
175
|
+
}
|
|
176
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
177
|
+
let danglingSum = 0;
|
|
178
|
+
for (const id of danglingNodes) {
|
|
179
|
+
danglingSum += ranks.get(id) ?? 0;
|
|
180
|
+
}
|
|
181
|
+
const danglingContrib = damping * danglingSum / N;
|
|
182
|
+
let maxDelta = 0;
|
|
183
|
+
if (useGS) {
|
|
184
|
+
for (const id of nodeIds) {
|
|
185
|
+
const biasVal = bias.get(id) ?? 1 / N;
|
|
186
|
+
let incomingSum = 0;
|
|
187
|
+
for (const src of inLinks.get(id) ?? []) {
|
|
188
|
+
const srcRank = ranks.get(src) ?? 0;
|
|
189
|
+
const srcOut = outDegree.get(src) ?? 1;
|
|
190
|
+
incomingSum += srcRank / srcOut;
|
|
191
|
+
}
|
|
192
|
+
const newRank = (1 - damping) * biasVal + damping * incomingSum + danglingContrib;
|
|
193
|
+
const oldRank = ranks.get(id) ?? 0;
|
|
194
|
+
const delta = Math.abs(newRank - oldRank);
|
|
195
|
+
if (delta > maxDelta) maxDelta = delta;
|
|
196
|
+
ranks.set(id, newRank);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const newRanks = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const id of nodeIds) {
|
|
201
|
+
const biasVal = bias.get(id) ?? 1 / N;
|
|
202
|
+
let incomingSum = 0;
|
|
203
|
+
for (const src of inLinks.get(id) ?? []) {
|
|
204
|
+
const srcRank = ranks.get(src) ?? 0;
|
|
205
|
+
const srcOut = outDegree.get(src) ?? 1;
|
|
206
|
+
incomingSum += srcRank / srcOut;
|
|
207
|
+
}
|
|
208
|
+
const newRank = (1 - damping) * biasVal + damping * incomingSum + danglingContrib;
|
|
209
|
+
newRanks.set(id, newRank);
|
|
210
|
+
const delta = Math.abs(newRank - (ranks.get(id) ?? 0));
|
|
211
|
+
if (delta > maxDelta) maxDelta = delta;
|
|
212
|
+
}
|
|
213
|
+
for (const [id, rank] of newRanks) ranks.set(id, rank);
|
|
214
|
+
}
|
|
215
|
+
if (maxDelta < epsilon) break;
|
|
216
|
+
}
|
|
217
|
+
return ranks;
|
|
218
|
+
}
|
|
219
|
+
var IncrementalPageRank = class {
|
|
220
|
+
ranks = /* @__PURE__ */ new Map();
|
|
221
|
+
outDegree = /* @__PURE__ */ new Map();
|
|
222
|
+
inLinks = /* @__PURE__ */ new Map();
|
|
223
|
+
/** Forward adjacency list: source → Set<target>. Avoids O(E) reverse lookup in getOutNeighbors(). */
|
|
224
|
+
outLinks = /* @__PURE__ */ new Map();
|
|
225
|
+
nodeSet = /* @__PURE__ */ new Set();
|
|
226
|
+
damping;
|
|
227
|
+
epsilon;
|
|
228
|
+
constructor(damping = 0.85, epsilon = 1e-6) {
|
|
229
|
+
this.damping = damping;
|
|
230
|
+
this.epsilon = epsilon;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Initialize from a knowledge graph. Computes full PageRank once.
|
|
234
|
+
*
|
|
235
|
+
* @param graph - Initial knowledge graph.
|
|
236
|
+
*/
|
|
237
|
+
initialize(graph) {
|
|
238
|
+
this.nodeSet.clear();
|
|
239
|
+
this.outDegree.clear();
|
|
240
|
+
this.inLinks.clear();
|
|
241
|
+
this.outLinks.clear();
|
|
242
|
+
for (const node of graph.nodes) {
|
|
243
|
+
this.nodeSet.add(node.id);
|
|
244
|
+
this.outDegree.set(node.id, 0);
|
|
245
|
+
this.inLinks.set(node.id, /* @__PURE__ */ new Set());
|
|
246
|
+
this.outLinks.set(node.id, /* @__PURE__ */ new Set());
|
|
247
|
+
}
|
|
248
|
+
for (const edge of graph.edges) {
|
|
249
|
+
if (this.nodeSet.has(edge.source) && this.nodeSet.has(edge.target)) {
|
|
250
|
+
this.outDegree.set(edge.source, (this.outDegree.get(edge.source) ?? 0) + 1);
|
|
251
|
+
this.inLinks.get(edge.target).add(edge.source);
|
|
252
|
+
this.outLinks.get(edge.source).add(edge.target);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.ranks = computePersonalizedPageRank(graph, void 0, {
|
|
256
|
+
damping: this.damping,
|
|
257
|
+
epsilon: this.epsilon
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Incrementally add an edge and update ranks via residual propagation.
|
|
262
|
+
*
|
|
263
|
+
* When edge (u -> v) is added:
|
|
264
|
+
* - Old contribution from u to each of its neighbors was PR(u)/L_old(u)
|
|
265
|
+
* - New contribution is PR(u)/L_new(u) where L_new = L_old + 1
|
|
266
|
+
* - Delta at v = +d * PR(u) / L_new(u)
|
|
267
|
+
* - Delta at each old neighbor w = -d * PR(u) * (1/L_new - 1/L_old)
|
|
268
|
+
*
|
|
269
|
+
* These deltas are injected as residuals and propagated.
|
|
270
|
+
*
|
|
271
|
+
* @param source - Source node ID.
|
|
272
|
+
* @param target - Target node ID.
|
|
273
|
+
*/
|
|
274
|
+
addEdge(source, target) {
|
|
275
|
+
if (!this.nodeSet.has(source)) {
|
|
276
|
+
this.nodeSet.add(source);
|
|
277
|
+
this.outDegree.set(source, 0);
|
|
278
|
+
this.inLinks.set(source, /* @__PURE__ */ new Set());
|
|
279
|
+
this.outLinks.set(source, /* @__PURE__ */ new Set());
|
|
280
|
+
this.ranks.set(source, 1 / this.nodeSet.size);
|
|
281
|
+
}
|
|
282
|
+
if (!this.nodeSet.has(target)) {
|
|
283
|
+
this.nodeSet.add(target);
|
|
284
|
+
this.outDegree.set(target, 0);
|
|
285
|
+
this.inLinks.set(target, /* @__PURE__ */ new Set());
|
|
286
|
+
this.outLinks.set(target, /* @__PURE__ */ new Set());
|
|
287
|
+
this.ranks.set(target, 1 / this.nodeSet.size);
|
|
288
|
+
}
|
|
289
|
+
const oldDeg = this.outDegree.get(source) ?? 0;
|
|
290
|
+
const newDeg = oldDeg + 1;
|
|
291
|
+
this.outDegree.set(source, newDeg);
|
|
292
|
+
this.inLinks.get(target).add(source);
|
|
293
|
+
this.outLinks.get(source).add(target);
|
|
294
|
+
const prU = this.ranks.get(source) ?? 0;
|
|
295
|
+
const residuals = /* @__PURE__ */ new Map();
|
|
296
|
+
residuals.set(target, this.damping * prU / newDeg);
|
|
297
|
+
if (oldDeg > 0) {
|
|
298
|
+
const shareDelta = prU * (1 / newDeg - 1 / oldDeg);
|
|
299
|
+
for (const neighbor of this.getOutNeighbors(source)) {
|
|
300
|
+
if (neighbor === target) continue;
|
|
301
|
+
const prev = residuals.get(neighbor) ?? 0;
|
|
302
|
+
residuals.set(neighbor, prev + this.damping * shareDelta);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.propagateResiduals(residuals);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Incrementally remove an edge and update ranks via residual propagation.
|
|
309
|
+
*
|
|
310
|
+
* @param source - Source node ID.
|
|
311
|
+
* @param target - Target node ID.
|
|
312
|
+
*/
|
|
313
|
+
removeEdge(source, target) {
|
|
314
|
+
if (!this.nodeSet.has(source) || !this.nodeSet.has(target)) return;
|
|
315
|
+
const inSet = this.inLinks.get(target);
|
|
316
|
+
if (!inSet || !inSet.has(source)) return;
|
|
317
|
+
const oldDeg = this.outDegree.get(source) ?? 0;
|
|
318
|
+
if (oldDeg <= 0) return;
|
|
319
|
+
const newDeg = oldDeg - 1;
|
|
320
|
+
this.outDegree.set(source, newDeg);
|
|
321
|
+
inSet.delete(source);
|
|
322
|
+
this.outLinks.get(source)?.delete(target);
|
|
323
|
+
const prU = this.ranks.get(source) ?? 0;
|
|
324
|
+
const residuals = /* @__PURE__ */ new Map();
|
|
325
|
+
residuals.set(target, -this.damping * prU / oldDeg);
|
|
326
|
+
if (newDeg > 0) {
|
|
327
|
+
const shareDelta = prU * (1 / newDeg - 1 / oldDeg);
|
|
328
|
+
for (const neighbor of this.getOutNeighbors(source)) {
|
|
329
|
+
const prev = residuals.get(neighbor) ?? 0;
|
|
330
|
+
residuals.set(neighbor, prev + this.damping * shareDelta);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
this.propagateResiduals(residuals);
|
|
334
|
+
}
|
|
335
|
+
/** Get current rank scores. */
|
|
336
|
+
getRanks() {
|
|
337
|
+
return new Map(this.ranks);
|
|
338
|
+
}
|
|
339
|
+
// ─── Private ──────────────────────────────────────────────────────
|
|
340
|
+
/**
|
|
341
|
+
* Propagate residuals through the graph until all are below epsilon.
|
|
342
|
+
*
|
|
343
|
+
* Push-forward scheme: the node with largest absolute residual pushes
|
|
344
|
+
* its residual to its rank and propagates damped fractions to neighbors.
|
|
345
|
+
*
|
|
346
|
+
* Convergence: guaranteed for damping < 1, with total work O(1/epsilon).
|
|
347
|
+
*/
|
|
348
|
+
propagateResiduals(residuals) {
|
|
349
|
+
const maxPropagations = this.nodeSet.size * 20;
|
|
350
|
+
let propagations = 0;
|
|
351
|
+
while (propagations < maxPropagations) {
|
|
352
|
+
let maxNode = null;
|
|
353
|
+
let maxResidual = 0;
|
|
354
|
+
for (const [node, res2] of residuals) {
|
|
355
|
+
const absRes = Math.abs(res2);
|
|
356
|
+
if (absRes > maxResidual) {
|
|
357
|
+
maxResidual = absRes;
|
|
358
|
+
maxNode = node;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (maxNode === null || maxResidual < this.epsilon) break;
|
|
362
|
+
const res = residuals.get(maxNode);
|
|
363
|
+
this.ranks.set(maxNode, (this.ranks.get(maxNode) ?? 0) + res);
|
|
364
|
+
residuals.delete(maxNode);
|
|
365
|
+
const outDeg = this.outDegree.get(maxNode) ?? 0;
|
|
366
|
+
if (outDeg > 0) {
|
|
367
|
+
const propagated = this.damping * res / outDeg;
|
|
368
|
+
for (const neighbor of this.getOutNeighbors(maxNode)) {
|
|
369
|
+
const prevRes = residuals.get(neighbor) ?? 0;
|
|
370
|
+
residuals.set(neighbor, prevRes + propagated);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
propagations++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Get outgoing neighbors of a node.
|
|
378
|
+
* Uses forward adjacency list for O(degree) lookup instead of O(E).
|
|
379
|
+
*/
|
|
380
|
+
getOutNeighbors(nodeId) {
|
|
381
|
+
return [...this.outLinks.get(nodeId) ?? []];
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// ../chitragupta/packages/smriti/src/graphrag-extraction.ts
|
|
386
|
+
var CHUNK_TARGET_TOKENS = 350;
|
|
387
|
+
var CHUNK_MIN_TOKENS = 200;
|
|
388
|
+
var CHUNK_MAX_TOKENS = 500;
|
|
389
|
+
var CHUNK_OVERLAP_TOKENS = 50;
|
|
390
|
+
function splitSentences(text) {
|
|
391
|
+
const raw = text.split(/(?<=[.!?])\s+(?=[A-Z])|(?<=[.!?])\s*$/);
|
|
392
|
+
return raw.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
393
|
+
}
|
|
394
|
+
function estimateTokens(text) {
|
|
395
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
396
|
+
}
|
|
397
|
+
function semanticChunk(text) {
|
|
398
|
+
const sentences = splitSentences(text);
|
|
399
|
+
if (sentences.length === 0) return [];
|
|
400
|
+
const totalTokens = estimateTokens(text);
|
|
401
|
+
if (totalTokens <= CHUNK_MAX_TOKENS) {
|
|
402
|
+
return [{ text, startSentence: 0, endSentence: sentences.length - 1 }];
|
|
403
|
+
}
|
|
404
|
+
const chunks = [];
|
|
405
|
+
let startIdx = 0;
|
|
406
|
+
while (startIdx < sentences.length) {
|
|
407
|
+
let currentTokens = 0;
|
|
408
|
+
let endIdx = startIdx;
|
|
409
|
+
while (endIdx < sentences.length && currentTokens < CHUNK_TARGET_TOKENS) {
|
|
410
|
+
currentTokens += estimateTokens(sentences[endIdx]);
|
|
411
|
+
endIdx++;
|
|
412
|
+
}
|
|
413
|
+
if (currentTokens < CHUNK_MIN_TOKENS && endIdx < sentences.length) {
|
|
414
|
+
while (endIdx < sentences.length && currentTokens < CHUNK_MIN_TOKENS) {
|
|
415
|
+
currentTokens += estimateTokens(sentences[endIdx]);
|
|
416
|
+
endIdx++;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const chunkText = sentences.slice(startIdx, endIdx).join(" ");
|
|
420
|
+
chunks.push({ text: chunkText, startSentence: startIdx, endSentence: endIdx - 1 });
|
|
421
|
+
let overlapTokens = 0;
|
|
422
|
+
let overlapStart = endIdx;
|
|
423
|
+
while (overlapStart > startIdx && overlapTokens < CHUNK_OVERLAP_TOKENS) {
|
|
424
|
+
overlapStart--;
|
|
425
|
+
overlapTokens += estimateTokens(sentences[overlapStart]);
|
|
426
|
+
}
|
|
427
|
+
startIdx = Math.max(overlapStart, startIdx + 1);
|
|
428
|
+
if (startIdx >= endIdx) break;
|
|
429
|
+
}
|
|
430
|
+
return chunks;
|
|
431
|
+
}
|
|
432
|
+
async function llmExtractEntities(text, endpoint, generationModel) {
|
|
433
|
+
const truncated = text.slice(0, 4e3);
|
|
434
|
+
const prompt = `Extract key entities, concepts, and topics from the following text. Return ONLY a valid JSON array of objects, each with "name" (string), "type" (one of: "person", "technology", "concept", "organization", "file", "tool", "decision", "topic"), and "description" (brief string). Do not include any text outside the JSON array.
|
|
435
|
+
|
|
436
|
+
Text:
|
|
437
|
+
${truncated}
|
|
438
|
+
|
|
439
|
+
JSON:`;
|
|
440
|
+
const response = await fetch(`${endpoint}/api/generate`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { "Content-Type": "application/json" },
|
|
443
|
+
body: JSON.stringify({
|
|
444
|
+
model: generationModel,
|
|
445
|
+
prompt,
|
|
446
|
+
stream: false,
|
|
447
|
+
options: {
|
|
448
|
+
temperature: 0.1,
|
|
449
|
+
num_predict: 2048
|
|
450
|
+
}
|
|
451
|
+
}),
|
|
452
|
+
signal: AbortSignal.timeout(6e4)
|
|
453
|
+
});
|
|
454
|
+
if (!response.ok) {
|
|
455
|
+
throw new Error(`Ollama generation error: ${response.status}`);
|
|
456
|
+
}
|
|
457
|
+
const data = await response.json();
|
|
458
|
+
const responseText = data.response.trim();
|
|
459
|
+
const jsonMatch = responseText.match(/\[[\s\S]*\]/);
|
|
460
|
+
if (!jsonMatch) {
|
|
461
|
+
throw new Error("LLM response did not contain a JSON array");
|
|
462
|
+
}
|
|
463
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
464
|
+
const entities = [];
|
|
465
|
+
for (const item of parsed) {
|
|
466
|
+
if (typeof item === "object" && item !== null && "name" in item && typeof item.name === "string") {
|
|
467
|
+
const obj = item;
|
|
468
|
+
entities.push({
|
|
469
|
+
name: String(obj.name).toLowerCase().trim(),
|
|
470
|
+
type: typeof obj.type === "string" ? obj.type : "concept",
|
|
471
|
+
description: typeof obj.description === "string" ? obj.description : ""
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return entities;
|
|
476
|
+
}
|
|
477
|
+
function keywordExtractEntities(text) {
|
|
478
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 4 && !STOP_WORDS.has(w));
|
|
479
|
+
const freq = /* @__PURE__ */ new Map();
|
|
480
|
+
for (const word of words) {
|
|
481
|
+
freq.set(word, (freq.get(word) ?? 0) + 1);
|
|
482
|
+
}
|
|
483
|
+
const entities = [];
|
|
484
|
+
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
|
|
485
|
+
for (const [word, count] of sorted) {
|
|
486
|
+
if (count < 2) break;
|
|
487
|
+
if (entities.length >= 20) break;
|
|
488
|
+
entities.push({
|
|
489
|
+
name: word,
|
|
490
|
+
type: "concept",
|
|
491
|
+
description: `Keyword appearing ${count} times`
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return entities;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ../chitragupta/packages/smriti/src/ner-extractor.ts
|
|
498
|
+
var DEFAULT_ENDPOINT = "http://localhost:8501";
|
|
499
|
+
var DEFAULT_MODEL = "gliner-large-v2.1";
|
|
500
|
+
var DEFAULT_MIN_CONFIDENCE = 0.5;
|
|
501
|
+
var DEFAULT_MAX_ENTITIES = 50;
|
|
502
|
+
var GLINER_TIMEOUT_MS = 1e4;
|
|
503
|
+
var GLINER_PROBE_TIMEOUT_MS = 3e3;
|
|
504
|
+
var ALL_ENTITY_TYPES = [
|
|
505
|
+
"person",
|
|
506
|
+
"tool",
|
|
507
|
+
"file",
|
|
508
|
+
"concept",
|
|
509
|
+
"decision",
|
|
510
|
+
"error",
|
|
511
|
+
"technology",
|
|
512
|
+
"action",
|
|
513
|
+
"location",
|
|
514
|
+
"organization"
|
|
515
|
+
];
|
|
516
|
+
var GLINER_LABEL_MAP = {
|
|
517
|
+
// Standard NER tags
|
|
518
|
+
per: "person",
|
|
519
|
+
person: "person",
|
|
520
|
+
org: "organization",
|
|
521
|
+
organization: "organization",
|
|
522
|
+
loc: "location",
|
|
523
|
+
location: "location",
|
|
524
|
+
gpe: "location",
|
|
525
|
+
// Our custom labels (sent as GLiNER request labels)
|
|
526
|
+
tool: "tool",
|
|
527
|
+
file: "file",
|
|
528
|
+
concept: "concept",
|
|
529
|
+
decision: "decision",
|
|
530
|
+
error: "error",
|
|
531
|
+
technology: "technology",
|
|
532
|
+
action: "action"
|
|
533
|
+
};
|
|
534
|
+
var HEURISTIC_CONFIDENCE = 0.6;
|
|
535
|
+
var FILE_PATTERN = /(?:^|[\s"'`(])([./][\w/.-]+\.\w{1,10})\b/g;
|
|
536
|
+
var TECHNOLOGY_WORDS = [
|
|
537
|
+
"angular",
|
|
538
|
+
"aws",
|
|
539
|
+
"azure",
|
|
540
|
+
"bash",
|
|
541
|
+
"bun",
|
|
542
|
+
"c\\+\\+",
|
|
543
|
+
"css",
|
|
544
|
+
"deno",
|
|
545
|
+
"docker",
|
|
546
|
+
"elasticsearch",
|
|
547
|
+
"express",
|
|
548
|
+
"fastify",
|
|
549
|
+
"firebase",
|
|
550
|
+
"git",
|
|
551
|
+
"go",
|
|
552
|
+
"golang",
|
|
553
|
+
"graphql",
|
|
554
|
+
"html",
|
|
555
|
+
"java",
|
|
556
|
+
"javascript",
|
|
557
|
+
"jest",
|
|
558
|
+
"kafka",
|
|
559
|
+
"kotlin",
|
|
560
|
+
"kubernetes",
|
|
561
|
+
"linux",
|
|
562
|
+
"mongodb",
|
|
563
|
+
"mysql",
|
|
564
|
+
"nest",
|
|
565
|
+
"next\\.js",
|
|
566
|
+
"nextjs",
|
|
567
|
+
"node",
|
|
568
|
+
"node\\.js",
|
|
569
|
+
"nodejs",
|
|
570
|
+
"npm",
|
|
571
|
+
"ollama",
|
|
572
|
+
"postgres",
|
|
573
|
+
"postgresql",
|
|
574
|
+
"prisma",
|
|
575
|
+
"python",
|
|
576
|
+
"react",
|
|
577
|
+
"redis",
|
|
578
|
+
"rust",
|
|
579
|
+
"sql",
|
|
580
|
+
"svelte",
|
|
581
|
+
"swift",
|
|
582
|
+
"terraform",
|
|
583
|
+
"typescript",
|
|
584
|
+
"vite",
|
|
585
|
+
"vitest",
|
|
586
|
+
"vue",
|
|
587
|
+
"webpack",
|
|
588
|
+
"yarn",
|
|
589
|
+
"zod"
|
|
590
|
+
];
|
|
591
|
+
var TECHNOLOGY_PATTERN = new RegExp(
|
|
592
|
+
`\\b(${TECHNOLOGY_WORDS.join("|")})\\b`,
|
|
593
|
+
"gi"
|
|
594
|
+
);
|
|
595
|
+
var TOOL_WORDS = [
|
|
596
|
+
"read",
|
|
597
|
+
"write",
|
|
598
|
+
"edit",
|
|
599
|
+
"bash",
|
|
600
|
+
"grep",
|
|
601
|
+
"find",
|
|
602
|
+
"ls",
|
|
603
|
+
"diff",
|
|
604
|
+
"watch",
|
|
605
|
+
"glob",
|
|
606
|
+
"notebookedit",
|
|
607
|
+
"webfetch",
|
|
608
|
+
"websearch"
|
|
609
|
+
];
|
|
610
|
+
var TOOL_PATTERN = new RegExp(
|
|
611
|
+
`\\b(${TOOL_WORDS.join("|")})\\b`,
|
|
612
|
+
"gi"
|
|
613
|
+
);
|
|
614
|
+
var ERROR_PATTERN = /\b((?:[A-Z]\w*Error)\b|(?:ENOENT|EACCES|ECONNREFUSED|ETIMEDOUT|EPERM)\b|(?:failed\s+to\s+\w+)|(?:error:\s*\S+))/gi;
|
|
615
|
+
var DECISION_PATTERN = /\b((?:decided|chose|selected|opted|switched|migrated|moved)\s+to\s+\w[\w\s]{0,30})/gi;
|
|
616
|
+
var ACTION_PATTERN = /\b((?:created|deleted|removed|modified|updated|installed|uninstalled|deployed|fixed|refactored|renamed|merged|rebased|reverted|committed|pushed|pulled)\s+\w[\w\s]{0,30})/gi;
|
|
617
|
+
var CONCEPT_PATTERN = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3})\b/g;
|
|
618
|
+
var NERExtractor = class {
|
|
619
|
+
endpoint;
|
|
620
|
+
model;
|
|
621
|
+
entityTypes;
|
|
622
|
+
minConfidence;
|
|
623
|
+
maxEntities;
|
|
624
|
+
useHeuristic;
|
|
625
|
+
/** Cached GLiNER2 availability probe result (null = not yet probed). */
|
|
626
|
+
glinerAvailable = null;
|
|
627
|
+
constructor(config = {}) {
|
|
628
|
+
this.endpoint = config.glinerEndpoint ?? DEFAULT_ENDPOINT;
|
|
629
|
+
this.model = config.glinerModel ?? DEFAULT_MODEL;
|
|
630
|
+
this.entityTypes = config.entityTypes ?? ALL_ENTITY_TYPES;
|
|
631
|
+
this.minConfidence = config.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
632
|
+
this.maxEntities = config.maxEntities ?? DEFAULT_MAX_ENTITIES;
|
|
633
|
+
this.useHeuristic = config.useHeuristic ?? true;
|
|
634
|
+
}
|
|
635
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
636
|
+
/**
|
|
637
|
+
* Extract entities from text.
|
|
638
|
+
* Tries GLiNER2 first; falls back to heuristic if GLiNER2 is unavailable
|
|
639
|
+
* and `useHeuristic` is enabled.
|
|
640
|
+
*/
|
|
641
|
+
async extract(text) {
|
|
642
|
+
if (!text || text.trim().length === 0) return [];
|
|
643
|
+
const glinerUp = await this.isGLiNERAvailable();
|
|
644
|
+
if (glinerUp) {
|
|
645
|
+
try {
|
|
646
|
+
const entities = await this.extractViaGLiNER(text);
|
|
647
|
+
return this.postProcess(entities);
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (this.useHeuristic) {
|
|
652
|
+
const entities = this.extractViaHeuristic(text);
|
|
653
|
+
return this.postProcess(entities);
|
|
654
|
+
}
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Extract entities from multiple texts in batch.
|
|
659
|
+
* Each text is processed independently; results are returned in order.
|
|
660
|
+
*/
|
|
661
|
+
async extractBatch(texts) {
|
|
662
|
+
return Promise.all(texts.map((t) => this.extract(t)));
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Probe whether GLiNER2 is reachable at the configured endpoint.
|
|
666
|
+
* Caches the result after the first successful probe.
|
|
667
|
+
*/
|
|
668
|
+
async isGLiNERAvailable() {
|
|
669
|
+
if (this.glinerAvailable !== null) return this.glinerAvailable;
|
|
670
|
+
try {
|
|
671
|
+
const response = await fetch(this.endpoint, {
|
|
672
|
+
method: "GET",
|
|
673
|
+
signal: AbortSignal.timeout(GLINER_PROBE_TIMEOUT_MS)
|
|
674
|
+
});
|
|
675
|
+
this.glinerAvailable = response.ok;
|
|
676
|
+
} catch {
|
|
677
|
+
this.glinerAvailable = false;
|
|
678
|
+
}
|
|
679
|
+
return this.glinerAvailable;
|
|
680
|
+
}
|
|
681
|
+
// ─── GLiNER2 Integration ─────────────────────────────────────────────────
|
|
682
|
+
/**
|
|
683
|
+
* Call GLiNER2's /predict endpoint to extract entities.
|
|
684
|
+
*
|
|
685
|
+
* Request format:
|
|
686
|
+
* POST /predict { text, labels, model }
|
|
687
|
+
*
|
|
688
|
+
* Response format:
|
|
689
|
+
* { entities: [{ text, label, score, start, end }] }
|
|
690
|
+
*/
|
|
691
|
+
async extractViaGLiNER(text) {
|
|
692
|
+
const response = await fetch(`${this.endpoint}/predict`, {
|
|
693
|
+
method: "POST",
|
|
694
|
+
headers: { "Content-Type": "application/json" },
|
|
695
|
+
body: JSON.stringify({
|
|
696
|
+
text,
|
|
697
|
+
labels: this.entityTypes,
|
|
698
|
+
model: this.model
|
|
699
|
+
}),
|
|
700
|
+
signal: AbortSignal.timeout(GLINER_TIMEOUT_MS)
|
|
701
|
+
});
|
|
702
|
+
if (!response.ok) {
|
|
703
|
+
throw new Error(`GLiNER2 prediction error: ${response.status}`);
|
|
704
|
+
}
|
|
705
|
+
const data = await response.json();
|
|
706
|
+
if (!data.entities || !Array.isArray(data.entities)) {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
return data.entities.map((e) => ({
|
|
710
|
+
text: e.text,
|
|
711
|
+
type: this.mapGLiNERLabel(e.label),
|
|
712
|
+
confidence: e.score,
|
|
713
|
+
span: [e.start, e.end]
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Map a GLiNER2 label string to our EntityType enum.
|
|
718
|
+
* Falls back to "concept" for unknown labels.
|
|
719
|
+
*/
|
|
720
|
+
mapGLiNERLabel(label) {
|
|
721
|
+
const normalized = label.toLowerCase().trim();
|
|
722
|
+
return GLINER_LABEL_MAP[normalized] ?? "concept";
|
|
723
|
+
}
|
|
724
|
+
// ─── Heuristic Fallback ──────────────────────────────────────────────────
|
|
725
|
+
/**
|
|
726
|
+
* Regex-based NER extraction.
|
|
727
|
+
* Each pattern extracts entities of a specific type.
|
|
728
|
+
* All heuristic matches receive a fixed confidence of 0.6.
|
|
729
|
+
*/
|
|
730
|
+
extractViaHeuristic(text) {
|
|
731
|
+
const entities = [];
|
|
732
|
+
const coveredSpans = [];
|
|
733
|
+
const addMatch = (match, type, groupIndex = 1) => {
|
|
734
|
+
const captured = match[groupIndex];
|
|
735
|
+
if (!captured) return;
|
|
736
|
+
const start = match.index + match[0].indexOf(captured);
|
|
737
|
+
const end = start + captured.length;
|
|
738
|
+
if (this.spanOverlaps([start, end], coveredSpans)) return;
|
|
739
|
+
coveredSpans.push([start, end]);
|
|
740
|
+
entities.push({
|
|
741
|
+
text: captured.trim(),
|
|
742
|
+
type,
|
|
743
|
+
confidence: HEURISTIC_CONFIDENCE,
|
|
744
|
+
span: [start, end]
|
|
745
|
+
});
|
|
746
|
+
};
|
|
747
|
+
this.execAll(FILE_PATTERN, text, (m) => addMatch(m, "file"));
|
|
748
|
+
this.execAll(ERROR_PATTERN, text, (m) => addMatch(m, "error"));
|
|
749
|
+
this.execAll(TECHNOLOGY_PATTERN, text, (m) => addMatch(m, "technology"));
|
|
750
|
+
this.execAll(TOOL_PATTERN, text, (m) => addMatch(m, "tool"));
|
|
751
|
+
this.execAll(DECISION_PATTERN, text, (m) => addMatch(m, "decision"));
|
|
752
|
+
this.execAll(ACTION_PATTERN, text, (m) => addMatch(m, "action"));
|
|
753
|
+
this.execAll(CONCEPT_PATTERN, text, (m) => addMatch(m, "concept"));
|
|
754
|
+
return entities;
|
|
755
|
+
}
|
|
756
|
+
// ─── Utilities ───────────────────────────────────────────────────────────
|
|
757
|
+
/**
|
|
758
|
+
* Execute a global regex against text, calling `fn` for each match.
|
|
759
|
+
* Resets the regex lastIndex before and after execution.
|
|
760
|
+
*/
|
|
761
|
+
execAll(pattern, text, fn) {
|
|
762
|
+
pattern.lastIndex = 0;
|
|
763
|
+
let match;
|
|
764
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
765
|
+
fn(match);
|
|
766
|
+
}
|
|
767
|
+
pattern.lastIndex = 0;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Check if a candidate span overlaps with any existing span.
|
|
771
|
+
*/
|
|
772
|
+
spanOverlaps(candidate, existing) {
|
|
773
|
+
const [cs, ce] = candidate;
|
|
774
|
+
return existing.some(([es, ee]) => cs < ee && ce > es);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Post-process extracted entities:
|
|
778
|
+
* 1. Filter by requested entity types
|
|
779
|
+
* 2. Filter by minimum confidence
|
|
780
|
+
* 3. Deduplicate by normalized text
|
|
781
|
+
* 4. Sort by confidence descending
|
|
782
|
+
* 5. Cap at maxEntities
|
|
783
|
+
*/
|
|
784
|
+
postProcess(entities) {
|
|
785
|
+
const typeSet = new Set(this.entityTypes);
|
|
786
|
+
let filtered = entities.filter(
|
|
787
|
+
(e) => typeSet.has(e.type) && e.confidence >= this.minConfidence
|
|
788
|
+
);
|
|
789
|
+
const seen = /* @__PURE__ */ new Map();
|
|
790
|
+
for (const entity of filtered) {
|
|
791
|
+
const key = `${entity.type}::${entity.text.toLowerCase().trim()}`;
|
|
792
|
+
const existing = seen.get(key);
|
|
793
|
+
if (!existing || entity.confidence > existing.confidence) {
|
|
794
|
+
seen.set(key, entity);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
filtered = [...seen.values()];
|
|
798
|
+
filtered.sort((a, b) => {
|
|
799
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
800
|
+
return a.span[0] - b.span[0];
|
|
801
|
+
});
|
|
802
|
+
return filtered.slice(0, this.maxEntities);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// ../chitragupta/packages/smriti/src/bitemporal.ts
|
|
807
|
+
function nowISO() {
|
|
808
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
809
|
+
}
|
|
810
|
+
var EPOCH_ISO = "1970-01-01T00:00:00.000Z";
|
|
811
|
+
function createEdge(source, target, relationship, weight, validFrom) {
|
|
812
|
+
const now = nowISO();
|
|
813
|
+
return {
|
|
814
|
+
source,
|
|
815
|
+
target,
|
|
816
|
+
relationship,
|
|
817
|
+
weight,
|
|
818
|
+
validFrom: validFrom ?? now,
|
|
819
|
+
recordedAt: now
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
function supersedEdge(existingEdge, newWeight, newRelationship) {
|
|
823
|
+
const now = nowISO();
|
|
824
|
+
const superseded = {
|
|
825
|
+
...existingEdge,
|
|
826
|
+
supersededAt: now
|
|
827
|
+
};
|
|
828
|
+
const replacement = {
|
|
829
|
+
source: existingEdge.source,
|
|
830
|
+
target: existingEdge.target,
|
|
831
|
+
relationship: newRelationship ?? existingEdge.relationship,
|
|
832
|
+
weight: newWeight ?? existingEdge.weight,
|
|
833
|
+
validFrom: now,
|
|
834
|
+
recordedAt: now
|
|
835
|
+
};
|
|
836
|
+
return [superseded, replacement];
|
|
837
|
+
}
|
|
838
|
+
function expireEdge(edge, validUntil) {
|
|
839
|
+
return {
|
|
840
|
+
...edge,
|
|
841
|
+
validUntil: validUntil ?? nowISO()
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function queryEdgesAtTime(edges, asOfValid, asOfRecord) {
|
|
845
|
+
return edges.filter((e) => {
|
|
846
|
+
const vFrom = e.validFrom ?? EPOCH_ISO;
|
|
847
|
+
if (vFrom > asOfValid) return false;
|
|
848
|
+
if (e.validUntil !== void 0 && e.validUntil <= asOfValid) return false;
|
|
849
|
+
if (asOfRecord !== void 0) {
|
|
850
|
+
const rAt = e.recordedAt ?? EPOCH_ISO;
|
|
851
|
+
if (rAt > asOfRecord) return false;
|
|
852
|
+
if (e.supersededAt !== void 0 && e.supersededAt <= asOfRecord) return false;
|
|
853
|
+
} else {
|
|
854
|
+
if (e.supersededAt !== void 0) return false;
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
function getEdgeHistory(edges, source, target) {
|
|
860
|
+
return edges.filter((e) => e.source === source && e.target === target).sort((a, b) => (a.recordedAt ?? EPOCH_ISO).localeCompare(b.recordedAt ?? EPOCH_ISO));
|
|
861
|
+
}
|
|
862
|
+
function temporalDecay(edge, now, halfLifeMs) {
|
|
863
|
+
const referenceTime = edge.validUntil !== void 0 ? new Date(edge.validUntil).getTime() : new Date(edge.validFrom ?? EPOCH_ISO).getTime();
|
|
864
|
+
const elapsed = now - referenceTime;
|
|
865
|
+
if (elapsed <= 0) return edge.weight;
|
|
866
|
+
const decay = Math.exp(-Math.LN2 * elapsed / halfLifeMs);
|
|
867
|
+
return edge.weight * decay;
|
|
868
|
+
}
|
|
869
|
+
function compactEdges(edges, retentionMs) {
|
|
870
|
+
const cutoff = Date.now() - retentionMs;
|
|
871
|
+
return edges.filter((e) => {
|
|
872
|
+
if (e.supersededAt === void 0) return true;
|
|
873
|
+
return new Date(e.supersededAt).getTime() > cutoff;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ../chitragupta/packages/smriti/src/graphrag-builder.ts
|
|
878
|
+
function scopeToId(scope) {
|
|
879
|
+
switch (scope.type) {
|
|
880
|
+
case "global":
|
|
881
|
+
return "memory-global";
|
|
882
|
+
case "project":
|
|
883
|
+
return `memory-project-${scope.path}`;
|
|
884
|
+
case "agent":
|
|
885
|
+
return `memory-agent-${scope.agentId}`;
|
|
886
|
+
case "session":
|
|
887
|
+
return `memory-session-${scope.sessionId}`;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function scopeToLabel(scope) {
|
|
891
|
+
switch (scope.type) {
|
|
892
|
+
case "global":
|
|
893
|
+
return "Global Memory";
|
|
894
|
+
case "project":
|
|
895
|
+
return `Project Memory: ${scope.path}`;
|
|
896
|
+
case "agent":
|
|
897
|
+
return `Agent Memory: ${scope.agentId}`;
|
|
898
|
+
case "session":
|
|
899
|
+
return `Session Memory: ${scope.sessionId}`;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
async function createSessionNode(ctx, session) {
|
|
903
|
+
const summaryText = `${session.meta.title} - ${session.meta.tags.join(", ")}`;
|
|
904
|
+
return {
|
|
905
|
+
id: session.meta.id,
|
|
906
|
+
type: "session",
|
|
907
|
+
label: session.meta.title,
|
|
908
|
+
content: summaryText,
|
|
909
|
+
embedding: await ctx.getEmbedding(summaryText),
|
|
910
|
+
metadata: {
|
|
911
|
+
agent: session.meta.agent,
|
|
912
|
+
model: session.meta.model,
|
|
913
|
+
project: session.meta.project,
|
|
914
|
+
created: session.meta.created,
|
|
915
|
+
turnCount: session.turns.length
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
async function extractConceptsFromNodes(ctx, nodes, edges) {
|
|
920
|
+
const entityIndex = /* @__PURE__ */ new Map();
|
|
921
|
+
for (const node of nodes) {
|
|
922
|
+
if (!node.content || node.type === "concept") continue;
|
|
923
|
+
const entities = await ctx.extractEntities(node.content);
|
|
924
|
+
for (const entity of entities) {
|
|
925
|
+
const key = entity.name.toLowerCase();
|
|
926
|
+
const existing = entityIndex.get(key);
|
|
927
|
+
if (existing) {
|
|
928
|
+
existing.sourceIds.push(node.id);
|
|
929
|
+
if (entity.description.length > existing.entity.description.length) {
|
|
930
|
+
existing.entity = entity;
|
|
931
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
entityIndex.set(key, { entity, sourceIds: [node.id] });
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
for (const [key, data] of entityIndex) {
|
|
938
|
+
if (data.sourceIds.length < 2) continue;
|
|
939
|
+
const conceptId = `concept-${key.replace(/\s+/g, "-")}`;
|
|
940
|
+
if (nodes.some((n) => n.id === conceptId)) continue;
|
|
941
|
+
const conceptNode = {
|
|
942
|
+
id: conceptId,
|
|
943
|
+
type: "concept",
|
|
944
|
+
label: data.entity.name,
|
|
945
|
+
content: data.entity.description || data.entity.name,
|
|
946
|
+
metadata: {
|
|
947
|
+
entityType: data.entity.type,
|
|
948
|
+
sourceCount: data.sourceIds.length
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
nodes.push(conceptNode);
|
|
952
|
+
for (const sourceId of data.sourceIds) {
|
|
953
|
+
edges.push(createEdge(sourceId, conceptId, "mentions_concept", 0.5));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async function indexSessionTurns(ctx, session, nodes, edges) {
|
|
958
|
+
for (const turn of session.turns) {
|
|
959
|
+
const chunks = semanticChunk(turn.content);
|
|
960
|
+
for (let chunkIdx = 0; chunkIdx < chunks.length; chunkIdx++) {
|
|
961
|
+
const chunk = chunks[chunkIdx];
|
|
962
|
+
const chunkId = `${session.meta.id}-turn-${turn.turnNumber}-chunk-${chunkIdx}`;
|
|
963
|
+
const chunkNode = {
|
|
964
|
+
id: chunkId,
|
|
965
|
+
type: "session",
|
|
966
|
+
label: `Turn ${turn.turnNumber} (${turn.role}) chunk ${chunkIdx + 1}/${chunks.length}`,
|
|
967
|
+
content: chunk.text,
|
|
968
|
+
embedding: await ctx.getEmbedding(chunk.text),
|
|
969
|
+
metadata: {
|
|
970
|
+
sessionId: session.meta.id,
|
|
971
|
+
turnNumber: turn.turnNumber,
|
|
972
|
+
role: turn.role,
|
|
973
|
+
chunkIndex: chunkIdx,
|
|
974
|
+
totalChunks: chunks.length,
|
|
975
|
+
startSentence: chunk.startSentence,
|
|
976
|
+
endSentence: chunk.endSentence
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
nodes.push(chunkNode);
|
|
980
|
+
edges.push(createEdge(session.meta.id, chunkId, "contains_chunk", 1));
|
|
981
|
+
if (chunkIdx > 0) {
|
|
982
|
+
const prevChunkId = `${session.meta.id}-turn-${turn.turnNumber}-chunk-${chunkIdx - 1}`;
|
|
983
|
+
edges.push(createEdge(prevChunkId, chunkId, "followed_by", 0.7));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (turn.toolCalls) {
|
|
987
|
+
for (let i = 0; i < turn.toolCalls.length; i++) {
|
|
988
|
+
const tc = turn.toolCalls[i];
|
|
989
|
+
const toolNodeId = `${session.meta.id}-turn-${turn.turnNumber}-tool-${i}`;
|
|
990
|
+
const toolNode = {
|
|
991
|
+
id: toolNodeId,
|
|
992
|
+
type: "file",
|
|
993
|
+
label: `Tool: ${tc.name}`,
|
|
994
|
+
content: tc.input.slice(0, 500),
|
|
995
|
+
metadata: {
|
|
996
|
+
toolName: tc.name,
|
|
997
|
+
isError: tc.isError ?? false
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
nodes.push(toolNode);
|
|
1001
|
+
const firstChunkId = `${session.meta.id}-turn-${turn.turnNumber}-chunk-0`;
|
|
1002
|
+
const linkTarget = nodes.some((n) => n.id === firstChunkId) ? firstChunkId : session.meta.id;
|
|
1003
|
+
edges.push(createEdge(linkTarget, toolNodeId, "uses_tool", 0.8));
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (session.meta.parent) {
|
|
1008
|
+
edges.push(createEdge(session.meta.parent, session.meta.id, "branched_to", 0.9));
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function buildMemoryNodes(ctx, memory, nodes, edges) {
|
|
1012
|
+
const memoryId = scopeToId(memory.scope);
|
|
1013
|
+
const chunks = semanticChunk(memory.content);
|
|
1014
|
+
if (chunks.length <= 1) {
|
|
1015
|
+
const memoryNode = {
|
|
1016
|
+
id: memoryId,
|
|
1017
|
+
type: "memory",
|
|
1018
|
+
label: scopeToLabel(memory.scope),
|
|
1019
|
+
content: memory.content,
|
|
1020
|
+
embedding: await ctx.getEmbedding(memory.content),
|
|
1021
|
+
metadata: { scope: memory.scope }
|
|
1022
|
+
};
|
|
1023
|
+
nodes.push(memoryNode);
|
|
1024
|
+
} else {
|
|
1025
|
+
const memoryNode = {
|
|
1026
|
+
id: memoryId,
|
|
1027
|
+
type: "memory",
|
|
1028
|
+
label: scopeToLabel(memory.scope),
|
|
1029
|
+
content: chunks[0].text,
|
|
1030
|
+
embedding: await ctx.getEmbedding(memory.content.slice(0, 500)),
|
|
1031
|
+
metadata: { scope: memory.scope, totalChunks: chunks.length }
|
|
1032
|
+
};
|
|
1033
|
+
nodes.push(memoryNode);
|
|
1034
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1035
|
+
const chunkId = `${memoryId}-chunk-${i}`;
|
|
1036
|
+
const chunkNode = {
|
|
1037
|
+
id: chunkId,
|
|
1038
|
+
type: "memory",
|
|
1039
|
+
label: `${scopeToLabel(memory.scope)} chunk ${i + 1}/${chunks.length}`,
|
|
1040
|
+
content: chunks[i].text,
|
|
1041
|
+
embedding: await ctx.getEmbedding(chunks[i].text),
|
|
1042
|
+
metadata: {
|
|
1043
|
+
scope: memory.scope,
|
|
1044
|
+
chunkIndex: i,
|
|
1045
|
+
totalChunks: chunks.length,
|
|
1046
|
+
startSentence: chunks[i].startSentence,
|
|
1047
|
+
endSentence: chunks[i].endSentence
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
nodes.push(chunkNode);
|
|
1051
|
+
edges.push(createEdge(memoryId, chunkId, "contains_chunk", 1));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
async function buildMemoryNodesFromContent(ctx, scope, content, nodes, edges) {
|
|
1056
|
+
const memoryId = scopeToId(scope);
|
|
1057
|
+
const chunks = semanticChunk(content);
|
|
1058
|
+
if (chunks.length <= 1) {
|
|
1059
|
+
const memoryNode = {
|
|
1060
|
+
id: memoryId,
|
|
1061
|
+
type: "memory",
|
|
1062
|
+
label: scopeToLabel(scope),
|
|
1063
|
+
content,
|
|
1064
|
+
embedding: await ctx.getEmbedding(content),
|
|
1065
|
+
metadata: { scope }
|
|
1066
|
+
};
|
|
1067
|
+
nodes.push(memoryNode);
|
|
1068
|
+
} else {
|
|
1069
|
+
const memoryNode = {
|
|
1070
|
+
id: memoryId,
|
|
1071
|
+
type: "memory",
|
|
1072
|
+
label: scopeToLabel(scope),
|
|
1073
|
+
content: chunks[0].text,
|
|
1074
|
+
embedding: await ctx.getEmbedding(content.slice(0, 500)),
|
|
1075
|
+
metadata: { scope, totalChunks: chunks.length }
|
|
1076
|
+
};
|
|
1077
|
+
nodes.push(memoryNode);
|
|
1078
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1079
|
+
const chunkId = `${memoryId}-chunk-${i}`;
|
|
1080
|
+
const chunkNode = {
|
|
1081
|
+
id: chunkId,
|
|
1082
|
+
type: "memory",
|
|
1083
|
+
label: `${scopeToLabel(scope)} chunk ${i + 1}/${chunks.length}`,
|
|
1084
|
+
content: chunks[i].text,
|
|
1085
|
+
embedding: await ctx.getEmbedding(chunks[i].text),
|
|
1086
|
+
metadata: {
|
|
1087
|
+
scope,
|
|
1088
|
+
chunkIndex: i,
|
|
1089
|
+
totalChunks: chunks.length,
|
|
1090
|
+
startSentence: chunks[i].startSentence,
|
|
1091
|
+
endSentence: chunks[i].endSentence
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
nodes.push(chunkNode);
|
|
1095
|
+
edges.push(createEdge(memoryId, chunkId, "contains_chunk", 1));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function removeSessionFromGraph(graph, sessionId) {
|
|
1100
|
+
const nodeIdsToRemove = /* @__PURE__ */ new Set();
|
|
1101
|
+
for (const node of graph.nodes) {
|
|
1102
|
+
if (node.id === sessionId || node.id.startsWith(`${sessionId}-turn-`)) {
|
|
1103
|
+
nodeIdsToRemove.add(node.id);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
graph.nodes = graph.nodes.filter((n) => !nodeIdsToRemove.has(n.id));
|
|
1107
|
+
graph.edges = graph.edges.filter(
|
|
1108
|
+
(e) => !nodeIdsToRemove.has(e.source) && !nodeIdsToRemove.has(e.target)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
function removeMemoryFromGraph(graph, scope) {
|
|
1112
|
+
const memoryId = scopeToId(scope);
|
|
1113
|
+
graph.nodes = graph.nodes.filter(
|
|
1114
|
+
(n) => n.id !== memoryId && !n.id.startsWith(`${memoryId}-chunk-`)
|
|
1115
|
+
);
|
|
1116
|
+
graph.edges = graph.edges.filter(
|
|
1117
|
+
(e) => e.source !== memoryId && e.target !== memoryId && !e.source.startsWith(`${memoryId}-chunk-`) && !e.target.startsWith(`${memoryId}-chunk-`)
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// ../chitragupta/packages/smriti/src/graphrag-leiden.ts
|
|
1122
|
+
var DEFAULT_LEIDEN_CONFIG = {
|
|
1123
|
+
resolution: 1,
|
|
1124
|
+
maxIterations: 10,
|
|
1125
|
+
minModularityGain: 1e-6,
|
|
1126
|
+
seed: 42,
|
|
1127
|
+
minCommunitySize: 1
|
|
1128
|
+
};
|
|
1129
|
+
var Xorshift32 = class {
|
|
1130
|
+
state;
|
|
1131
|
+
constructor(seed) {
|
|
1132
|
+
this.state = seed | 0 || 1;
|
|
1133
|
+
}
|
|
1134
|
+
next() {
|
|
1135
|
+
let x = this.state;
|
|
1136
|
+
x ^= x << 13;
|
|
1137
|
+
x ^= x >> 17;
|
|
1138
|
+
x ^= x << 5;
|
|
1139
|
+
this.state = x;
|
|
1140
|
+
return (x >>> 0) / 4294967295;
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
var AdjacencyGraph = class {
|
|
1144
|
+
/** nodeIndex → Map<neighborIndex, weight> */
|
|
1145
|
+
adj;
|
|
1146
|
+
/** nodeIndex → weighted degree (sum of edge weights) */
|
|
1147
|
+
degree;
|
|
1148
|
+
/** Total edge weight (2m). */
|
|
1149
|
+
totalWeight;
|
|
1150
|
+
/** Number of nodes. */
|
|
1151
|
+
n;
|
|
1152
|
+
/** Node ID → index mapping. */
|
|
1153
|
+
idToIndex;
|
|
1154
|
+
/** Index → node ID mapping. */
|
|
1155
|
+
indexToId;
|
|
1156
|
+
constructor(graph) {
|
|
1157
|
+
const nodeIds = graph.nodes.map((n) => n.id);
|
|
1158
|
+
this.n = nodeIds.length;
|
|
1159
|
+
this.idToIndex = new Map(nodeIds.map((id, i) => [id, i]));
|
|
1160
|
+
this.indexToId = nodeIds;
|
|
1161
|
+
this.adj = new Array(this.n);
|
|
1162
|
+
this.degree = new Array(this.n).fill(0);
|
|
1163
|
+
for (let i = 0; i < this.n; i++) {
|
|
1164
|
+
this.adj[i] = /* @__PURE__ */ new Map();
|
|
1165
|
+
}
|
|
1166
|
+
let totalW = 0;
|
|
1167
|
+
for (const edge of graph.edges) {
|
|
1168
|
+
const s = this.idToIndex.get(edge.source);
|
|
1169
|
+
const t = this.idToIndex.get(edge.target);
|
|
1170
|
+
if (s === void 0 || t === void 0) continue;
|
|
1171
|
+
if (s === t) continue;
|
|
1172
|
+
const w = edge.weight ?? 1;
|
|
1173
|
+
this.adj[s].set(t, (this.adj[s].get(t) ?? 0) + w);
|
|
1174
|
+
this.adj[t].set(s, (this.adj[t].get(s) ?? 0) + w);
|
|
1175
|
+
this.degree[s] += w;
|
|
1176
|
+
this.degree[t] += w;
|
|
1177
|
+
totalW += 2 * w;
|
|
1178
|
+
}
|
|
1179
|
+
this.totalWeight = totalW;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
function computeModularity(g, assignment, resolution) {
|
|
1183
|
+
const twoM = g.totalWeight;
|
|
1184
|
+
if (twoM === 0) return 0;
|
|
1185
|
+
const internalWeight = /* @__PURE__ */ new Map();
|
|
1186
|
+
const communityDegree = /* @__PURE__ */ new Map();
|
|
1187
|
+
for (let i = 0; i < g.n; i++) {
|
|
1188
|
+
const ci = assignment[i];
|
|
1189
|
+
communityDegree.set(ci, (communityDegree.get(ci) ?? 0) + g.degree[i]);
|
|
1190
|
+
for (const [j, w] of g.adj[i]) {
|
|
1191
|
+
if (assignment[j] === ci && j > i) {
|
|
1192
|
+
internalWeight.set(ci, (internalWeight.get(ci) ?? 0) + w);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
let Q = 0;
|
|
1197
|
+
for (const c of communityDegree.keys()) {
|
|
1198
|
+
const ec = internalWeight.get(c) ?? 0;
|
|
1199
|
+
const ac = communityDegree.get(c) ?? 0;
|
|
1200
|
+
Q += 2 * ec / twoM - resolution * (ac / twoM) ** 2;
|
|
1201
|
+
}
|
|
1202
|
+
return Q;
|
|
1203
|
+
}
|
|
1204
|
+
function modularityGain(g, i, cNew, assignment, communityDegree, resolution) {
|
|
1205
|
+
const twoM = g.totalWeight;
|
|
1206
|
+
if (twoM === 0) return 0;
|
|
1207
|
+
const cOld = assignment[i];
|
|
1208
|
+
if (cOld === cNew) return 0;
|
|
1209
|
+
const ki = g.degree[i];
|
|
1210
|
+
let kInNew = 0;
|
|
1211
|
+
let kInOld = 0;
|
|
1212
|
+
for (const [j, w] of g.adj[i]) {
|
|
1213
|
+
if (assignment[j] === cNew) kInNew += w;
|
|
1214
|
+
if (assignment[j] === cOld) kInOld += w;
|
|
1215
|
+
}
|
|
1216
|
+
const sigmaTotNew = communityDegree.get(cNew) ?? 0;
|
|
1217
|
+
const sigmaTotOld = communityDegree.get(cOld) ?? 0;
|
|
1218
|
+
const removeGain = -kInOld / twoM + resolution * ki * (sigmaTotOld - ki) / (twoM * twoM);
|
|
1219
|
+
const addGain = kInNew / twoM - resolution * ki * sigmaTotNew / (twoM * twoM);
|
|
1220
|
+
return removeGain + addGain;
|
|
1221
|
+
}
|
|
1222
|
+
function localNodeMoving(g, assignment, resolution, rng) {
|
|
1223
|
+
const communityDegree = /* @__PURE__ */ new Map();
|
|
1224
|
+
for (let i = 0; i < g.n; i++) {
|
|
1225
|
+
const ci = assignment[i];
|
|
1226
|
+
communityDegree.set(ci, (communityDegree.get(ci) ?? 0) + g.degree[i]);
|
|
1227
|
+
}
|
|
1228
|
+
const order = Array.from({ length: g.n }, (_, i) => i);
|
|
1229
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
1230
|
+
const j = Math.floor(rng.next() * (i + 1));
|
|
1231
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
1232
|
+
}
|
|
1233
|
+
let improved = false;
|
|
1234
|
+
for (const i of order) {
|
|
1235
|
+
const cOld = assignment[i];
|
|
1236
|
+
const ki = g.degree[i];
|
|
1237
|
+
const neighborCommunities = /* @__PURE__ */ new Set();
|
|
1238
|
+
for (const [j] of g.adj[i]) {
|
|
1239
|
+
neighborCommunities.add(assignment[j]);
|
|
1240
|
+
}
|
|
1241
|
+
let bestCommunity = cOld;
|
|
1242
|
+
let bestGain = 0;
|
|
1243
|
+
for (const cNew of neighborCommunities) {
|
|
1244
|
+
if (cNew === cOld) continue;
|
|
1245
|
+
const gain = modularityGain(g, i, cNew, assignment, communityDegree, resolution);
|
|
1246
|
+
if (gain > bestGain) {
|
|
1247
|
+
bestGain = gain;
|
|
1248
|
+
bestCommunity = cNew;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (bestCommunity !== cOld) {
|
|
1252
|
+
communityDegree.set(cOld, (communityDegree.get(cOld) ?? 0) - ki);
|
|
1253
|
+
communityDegree.set(bestCommunity, (communityDegree.get(bestCommunity) ?? 0) + ki);
|
|
1254
|
+
assignment[i] = bestCommunity;
|
|
1255
|
+
improved = true;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return improved;
|
|
1259
|
+
}
|
|
1260
|
+
function refineCommunities(g, assignment) {
|
|
1261
|
+
const communityNodes = /* @__PURE__ */ new Map();
|
|
1262
|
+
for (let i = 0; i < g.n; i++) {
|
|
1263
|
+
const c = assignment[i];
|
|
1264
|
+
if (!communityNodes.has(c)) communityNodes.set(c, []);
|
|
1265
|
+
communityNodes.get(c).push(i);
|
|
1266
|
+
}
|
|
1267
|
+
let nextCommunityId = Math.max(...assignment) + 1;
|
|
1268
|
+
for (const [_communityId, nodes] of communityNodes) {
|
|
1269
|
+
if (nodes.length <= 1) continue;
|
|
1270
|
+
const nodeSet = new Set(nodes);
|
|
1271
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1272
|
+
const components = [];
|
|
1273
|
+
for (const start of nodes) {
|
|
1274
|
+
if (visited.has(start)) continue;
|
|
1275
|
+
const component = [];
|
|
1276
|
+
const queue = [start];
|
|
1277
|
+
visited.add(start);
|
|
1278
|
+
while (queue.length > 0) {
|
|
1279
|
+
const node = queue.shift();
|
|
1280
|
+
component.push(node);
|
|
1281
|
+
for (const [neighbor] of g.adj[node]) {
|
|
1282
|
+
if (nodeSet.has(neighbor) && !visited.has(neighbor)) {
|
|
1283
|
+
visited.add(neighbor);
|
|
1284
|
+
queue.push(neighbor);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
components.push(component);
|
|
1289
|
+
}
|
|
1290
|
+
if (components.length > 1) {
|
|
1291
|
+
components.sort((a, b) => b.length - a.length);
|
|
1292
|
+
for (let k = 1; k < components.length; k++) {
|
|
1293
|
+
const newId = nextCommunityId++;
|
|
1294
|
+
for (const node of components[k]) {
|
|
1295
|
+
assignment[node] = newId;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function compactCommunities(assignment, minSize) {
|
|
1302
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
1303
|
+
for (const c of assignment) {
|
|
1304
|
+
sizes.set(c, (sizes.get(c) ?? 0) + 1);
|
|
1305
|
+
}
|
|
1306
|
+
let largestCommunity = 0;
|
|
1307
|
+
let largestSize = 0;
|
|
1308
|
+
for (const [c, s] of sizes) {
|
|
1309
|
+
if (s > largestSize) {
|
|
1310
|
+
largestSize = s;
|
|
1311
|
+
largestCommunity = c;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const remap = /* @__PURE__ */ new Map();
|
|
1315
|
+
let nextId = 0;
|
|
1316
|
+
for (const [c, s] of sizes) {
|
|
1317
|
+
if (s < minSize) {
|
|
1318
|
+
remap.set(c, remap.get(largestCommunity) ?? nextId);
|
|
1319
|
+
if (!remap.has(largestCommunity)) {
|
|
1320
|
+
remap.set(largestCommunity, nextId++);
|
|
1321
|
+
}
|
|
1322
|
+
} else {
|
|
1323
|
+
if (!remap.has(c)) {
|
|
1324
|
+
remap.set(c, nextId++);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return assignment.map((c) => remap.get(c) ?? 0);
|
|
1329
|
+
}
|
|
1330
|
+
function leiden(graph, config) {
|
|
1331
|
+
const cfg = { ...DEFAULT_LEIDEN_CONFIG, ...config };
|
|
1332
|
+
if (graph.nodes.length === 0) {
|
|
1333
|
+
return {
|
|
1334
|
+
communities: /* @__PURE__ */ new Map(),
|
|
1335
|
+
communityList: [],
|
|
1336
|
+
modularity: 0,
|
|
1337
|
+
iterations: 0,
|
|
1338
|
+
levels: 0
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const g = new AdjacencyGraph(graph);
|
|
1342
|
+
const rng = new Xorshift32(cfg.seed ?? Date.now() | 1);
|
|
1343
|
+
const assignment = Array.from({ length: g.n }, (_, i) => i);
|
|
1344
|
+
let prevModularity = -1;
|
|
1345
|
+
let iterations = 0;
|
|
1346
|
+
for (let iter = 0; iter < cfg.maxIterations; iter++) {
|
|
1347
|
+
iterations++;
|
|
1348
|
+
const improved = localNodeMoving(g, assignment, cfg.resolution, rng);
|
|
1349
|
+
refineCommunities(g, assignment);
|
|
1350
|
+
const currentModularity = computeModularity(g, assignment, cfg.resolution);
|
|
1351
|
+
if (!improved || currentModularity - prevModularity < cfg.minModularityGain) {
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
prevModularity = currentModularity;
|
|
1355
|
+
}
|
|
1356
|
+
const compacted = compactCommunities(assignment, cfg.minCommunitySize);
|
|
1357
|
+
const communities = /* @__PURE__ */ new Map();
|
|
1358
|
+
for (let i = 0; i < g.n; i++) {
|
|
1359
|
+
communities.set(g.indexToId[i], compacted[i]);
|
|
1360
|
+
}
|
|
1361
|
+
const finalModularity = computeModularity(g, compacted, cfg.resolution);
|
|
1362
|
+
const communityNodes = /* @__PURE__ */ new Map();
|
|
1363
|
+
for (let i = 0; i < g.n; i++) {
|
|
1364
|
+
const c = compacted[i];
|
|
1365
|
+
if (!communityNodes.has(c)) communityNodes.set(c, []);
|
|
1366
|
+
communityNodes.get(c).push(g.indexToId[i]);
|
|
1367
|
+
}
|
|
1368
|
+
const communityList = [];
|
|
1369
|
+
for (const [id, members] of communityNodes) {
|
|
1370
|
+
let internalEdges = 0;
|
|
1371
|
+
let internalWeight = 0;
|
|
1372
|
+
const memberSet = new Set(members.map((m) => g.idToIndex.get(m)));
|
|
1373
|
+
for (const mi of memberSet) {
|
|
1374
|
+
for (const [j, w] of g.adj[mi]) {
|
|
1375
|
+
if (memberSet.has(j) && j > mi) {
|
|
1376
|
+
internalEdges++;
|
|
1377
|
+
internalWeight += w;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const possibleEdges = members.length * (members.length - 1) / 2;
|
|
1382
|
+
const density = possibleEdges > 0 ? internalEdges / possibleEdges : 0;
|
|
1383
|
+
let ac = 0;
|
|
1384
|
+
for (const mi of memberSet) {
|
|
1385
|
+
ac += g.degree[mi];
|
|
1386
|
+
}
|
|
1387
|
+
const twoM = g.totalWeight;
|
|
1388
|
+
const communityMod = twoM > 0 ? 2 * internalWeight / twoM - cfg.resolution * (ac / twoM) ** 2 : 0;
|
|
1389
|
+
communityList.push({
|
|
1390
|
+
id,
|
|
1391
|
+
members,
|
|
1392
|
+
modularity: communityMod,
|
|
1393
|
+
internalDensity: density,
|
|
1394
|
+
level: 0
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
communityList.sort((a, b) => b.members.length - a.members.length);
|
|
1398
|
+
return {
|
|
1399
|
+
communities,
|
|
1400
|
+
communityList,
|
|
1401
|
+
modularity: finalModularity,
|
|
1402
|
+
iterations,
|
|
1403
|
+
levels: 1
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function annotateCommunities(graph, result) {
|
|
1407
|
+
for (const node of graph.nodes) {
|
|
1408
|
+
const communityId = result.communities.get(node.id);
|
|
1409
|
+
if (communityId !== void 0) {
|
|
1410
|
+
node.metadata.communityId = communityId;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function communitySummary(graph, communityId, maxLabels = 5) {
|
|
1415
|
+
const members = graph.nodes.filter(
|
|
1416
|
+
(n) => n.metadata.communityId === communityId
|
|
1417
|
+
);
|
|
1418
|
+
const typeCount = {};
|
|
1419
|
+
for (const node of members) {
|
|
1420
|
+
typeCount[node.type] = (typeCount[node.type] ?? 0) + 1;
|
|
1421
|
+
}
|
|
1422
|
+
const labels = members.filter((n) => n.type === "concept" || n.type === "session").slice(0, maxLabels).map((n) => n.label);
|
|
1423
|
+
return {
|
|
1424
|
+
communityId,
|
|
1425
|
+
labels,
|
|
1426
|
+
nodeTypes: typeCount,
|
|
1427
|
+
size: members.length
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function filterByCommunity(nodes, communityId) {
|
|
1431
|
+
return nodes.filter((n) => n.metadata.communityId === communityId);
|
|
1432
|
+
}
|
|
1433
|
+
function findBridgeNodes(graph, result, minCommunities = 2) {
|
|
1434
|
+
const touchedCommunities = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const node of graph.nodes) {
|
|
1436
|
+
const ownC = result.communities.get(node.id);
|
|
1437
|
+
if (ownC !== void 0) {
|
|
1438
|
+
touchedCommunities.set(node.id, /* @__PURE__ */ new Set([ownC]));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
for (const edge of graph.edges) {
|
|
1442
|
+
const sc = result.communities.get(edge.source);
|
|
1443
|
+
const tc = result.communities.get(edge.target);
|
|
1444
|
+
if (sc === void 0 || tc === void 0) continue;
|
|
1445
|
+
if (sc === tc) continue;
|
|
1446
|
+
touchedCommunities.get(edge.source)?.add(tc);
|
|
1447
|
+
touchedCommunities.get(edge.target)?.add(sc);
|
|
1448
|
+
}
|
|
1449
|
+
const bridges = [];
|
|
1450
|
+
for (const node of graph.nodes) {
|
|
1451
|
+
const communities = touchedCommunities.get(node.id);
|
|
1452
|
+
if (communities && communities.size >= minCommunities) {
|
|
1453
|
+
bridges.push(node);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return bridges;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ../chitragupta/packages/smriti/src/graphrag.ts
|
|
1460
|
+
var DEFAULT_CONFIG = {
|
|
1461
|
+
endpoint: process.env.OLLAMA_HOST ?? "http://localhost:11434",
|
|
1462
|
+
model: "nomic-embed-text",
|
|
1463
|
+
generationModel: "llama3.2"
|
|
1464
|
+
};
|
|
1465
|
+
function getGraphDir() {
|
|
1466
|
+
return path.join(getChitraguptaHome(), "graphrag");
|
|
1467
|
+
}
|
|
1468
|
+
function getGraphPath() {
|
|
1469
|
+
return path.join(getGraphDir(), "graph.json");
|
|
1470
|
+
}
|
|
1471
|
+
function getEmbeddingsPath() {
|
|
1472
|
+
return path.join(getGraphDir(), "embeddings.json");
|
|
1473
|
+
}
|
|
1474
|
+
function getPageRankPath() {
|
|
1475
|
+
return path.join(getGraphDir(), "pagerank.json");
|
|
1476
|
+
}
|
|
1477
|
+
var GraphRAGEngine = class {
|
|
1478
|
+
config;
|
|
1479
|
+
graph;
|
|
1480
|
+
embeddingCache;
|
|
1481
|
+
pageRankScores;
|
|
1482
|
+
maxEmbeddingCacheSize;
|
|
1483
|
+
ollamaAvailable = null;
|
|
1484
|
+
embeddingService;
|
|
1485
|
+
/** Push-based incremental PageRank engine — avoids full recompute on edge changes. */
|
|
1486
|
+
incrementalPR = null;
|
|
1487
|
+
/** Whether the graph.db schema has been initialized in this process. */
|
|
1488
|
+
_graphDbInitialized = false;
|
|
1489
|
+
constructor(config) {
|
|
1490
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1491
|
+
this.graph = { nodes: [], edges: [] };
|
|
1492
|
+
this.embeddingCache = /* @__PURE__ */ new Map();
|
|
1493
|
+
this.pageRankScores = /* @__PURE__ */ new Map();
|
|
1494
|
+
this.maxEmbeddingCacheSize = config?.maxEmbeddingCacheSize ?? 1e4;
|
|
1495
|
+
this.embeddingService = config?.embeddingService ?? new EmbeddingService();
|
|
1496
|
+
this.loadFromDisk();
|
|
1497
|
+
}
|
|
1498
|
+
// ─── Ollama Availability (for entity extraction) ──────────────────
|
|
1499
|
+
async checkOllamaAvailability() {
|
|
1500
|
+
if (this.ollamaAvailable !== null) return this.ollamaAvailable;
|
|
1501
|
+
try {
|
|
1502
|
+
const response = await fetch(`${this.config.endpoint}/api/version`, {
|
|
1503
|
+
method: "GET",
|
|
1504
|
+
signal: AbortSignal.timeout(3e3)
|
|
1505
|
+
});
|
|
1506
|
+
this.ollamaAvailable = response.ok;
|
|
1507
|
+
} catch {
|
|
1508
|
+
this.ollamaAvailable = false;
|
|
1509
|
+
}
|
|
1510
|
+
return this.ollamaAvailable;
|
|
1511
|
+
}
|
|
1512
|
+
async getEmbedding(text) {
|
|
1513
|
+
const cached = this.embeddingCache.get(text);
|
|
1514
|
+
if (cached) return cached;
|
|
1515
|
+
const vector = await this.embeddingService.getEmbedding(text);
|
|
1516
|
+
this.embeddingCache.set(text, vector);
|
|
1517
|
+
if (this.embeddingCache.size > this.maxEmbeddingCacheSize) {
|
|
1518
|
+
const oldest = this.embeddingCache.keys().next().value;
|
|
1519
|
+
if (oldest !== void 0) {
|
|
1520
|
+
this.embeddingCache.delete(oldest);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return vector;
|
|
1524
|
+
}
|
|
1525
|
+
// ─── Cosine Similarity (delegate) ──────────────────────────────────
|
|
1526
|
+
cosineSimilarity(a, b) {
|
|
1527
|
+
return cosineSimilarity(a, b);
|
|
1528
|
+
}
|
|
1529
|
+
// ─── Entity Extraction (delegates) ─────────────────────────────────
|
|
1530
|
+
async extractEntities(text) {
|
|
1531
|
+
const isAvailable = await this.checkOllamaAvailability();
|
|
1532
|
+
let baseEntities;
|
|
1533
|
+
if (isAvailable) {
|
|
1534
|
+
try {
|
|
1535
|
+
baseEntities = await llmExtractEntities(text, this.config.endpoint, this.config.generationModel);
|
|
1536
|
+
} catch {
|
|
1537
|
+
baseEntities = keywordExtractEntities(text);
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
baseEntities = keywordExtractEntities(text);
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
const nerExtractor = new NERExtractor({ useHeuristic: true });
|
|
1544
|
+
const nerEntities = await nerExtractor.extract(text);
|
|
1545
|
+
const nerTypeMap = {
|
|
1546
|
+
file: "file",
|
|
1547
|
+
technology: "concept",
|
|
1548
|
+
tool: "tool",
|
|
1549
|
+
error: "concept",
|
|
1550
|
+
decision: "concept",
|
|
1551
|
+
action: "concept",
|
|
1552
|
+
person: "person",
|
|
1553
|
+
organization: "organization",
|
|
1554
|
+
location: "concept",
|
|
1555
|
+
concept: "concept"
|
|
1556
|
+
};
|
|
1557
|
+
const seen = new Set(baseEntities.map((e) => e.name.toLowerCase()));
|
|
1558
|
+
for (const ne of nerEntities) {
|
|
1559
|
+
const normalized = ne.text.toLowerCase().trim();
|
|
1560
|
+
if (!seen.has(normalized) && normalized.length > 0) {
|
|
1561
|
+
baseEntities.push({
|
|
1562
|
+
name: normalized,
|
|
1563
|
+
type: nerTypeMap[ne.type] ?? "concept",
|
|
1564
|
+
description: `NER-detected ${ne.type} (confidence: ${ne.confidence.toFixed(2)})`
|
|
1565
|
+
});
|
|
1566
|
+
seen.add(normalized);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
return baseEntities;
|
|
1572
|
+
}
|
|
1573
|
+
// ─── Incremental PageRank ────────────────────────────────────────
|
|
1574
|
+
/**
|
|
1575
|
+
* Initialize (or reinitialize) the incremental PageRank engine from
|
|
1576
|
+
* the current graph state. Performs one full Gauss-Seidel computation
|
|
1577
|
+
* internally, then subsequent edge changes are O(1/ε) each via
|
|
1578
|
+
* push-based residual propagation.
|
|
1579
|
+
*/
|
|
1580
|
+
initIncrementalPR() {
|
|
1581
|
+
this.incrementalPR = new IncrementalPageRank();
|
|
1582
|
+
this.incrementalPR.initialize(this.graph);
|
|
1583
|
+
this.pageRankScores = this.incrementalPR.getRanks();
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Feed removed edges into the incremental PR engine so it can adjust
|
|
1587
|
+
* scores via residual propagation rather than a full recompute.
|
|
1588
|
+
*/
|
|
1589
|
+
applyEdgeRemovals(edges) {
|
|
1590
|
+
if (!this.incrementalPR || edges.length === 0) return;
|
|
1591
|
+
for (const edge of edges) {
|
|
1592
|
+
this.incrementalPR.removeEdge(edge.source, edge.target);
|
|
1593
|
+
}
|
|
1594
|
+
this.pageRankScores = this.incrementalPR.getRanks();
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Feed newly added edges into the incremental PR engine.
|
|
1598
|
+
*/
|
|
1599
|
+
applyEdgeAdditions(edges) {
|
|
1600
|
+
if (!this.incrementalPR || edges.length === 0) return;
|
|
1601
|
+
for (const edge of edges) {
|
|
1602
|
+
this.incrementalPR.addEdge(edge.source, edge.target);
|
|
1603
|
+
}
|
|
1604
|
+
this.pageRankScores = this.incrementalPR.getRanks();
|
|
1605
|
+
}
|
|
1606
|
+
// ─── PageRank (delegates) ──────────────────────────────────────────
|
|
1607
|
+
/**
|
|
1608
|
+
* Compute PageRank for the given graph (or the internal graph if none provided).
|
|
1609
|
+
*
|
|
1610
|
+
* When called with an explicit graph argument, uses the full power-iteration
|
|
1611
|
+
* algorithm (useful for one-off computations on external graphs and tests).
|
|
1612
|
+
* When called without arguments, initializes the incremental PageRank engine
|
|
1613
|
+
* on the internal graph.
|
|
1614
|
+
*/
|
|
1615
|
+
computePageRank(graph) {
|
|
1616
|
+
if (graph) {
|
|
1617
|
+
this.pageRankScores = computePageRank(graph);
|
|
1618
|
+
return this.pageRankScores;
|
|
1619
|
+
}
|
|
1620
|
+
this.initIncrementalPR();
|
|
1621
|
+
return this.pageRankScores;
|
|
1622
|
+
}
|
|
1623
|
+
getPageRank(nodeId) {
|
|
1624
|
+
return this.pageRankScores.get(nodeId) ?? 0;
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Force a full PageRank recompute from the current graph state.
|
|
1628
|
+
* Reinitializes the incremental engine from scratch. Useful after
|
|
1629
|
+
* graph compaction or any structural change that bypasses the
|
|
1630
|
+
* normal indexSession/indexMemory paths.
|
|
1631
|
+
*/
|
|
1632
|
+
recomputePageRank() {
|
|
1633
|
+
this.initIncrementalPR();
|
|
1634
|
+
this.saveToDisk();
|
|
1635
|
+
}
|
|
1636
|
+
// ─── Graph Building ───────────────────────────────────────────────
|
|
1637
|
+
async buildGraph(sessions, memories) {
|
|
1638
|
+
const nodes = [];
|
|
1639
|
+
const edges = [];
|
|
1640
|
+
for (const session of sessions) {
|
|
1641
|
+
const sessionNode = await createSessionNode(this, session);
|
|
1642
|
+
nodes.push(sessionNode);
|
|
1643
|
+
await indexSessionTurns(this, session, nodes, edges);
|
|
1644
|
+
}
|
|
1645
|
+
for (const memory of memories) {
|
|
1646
|
+
await buildMemoryNodes(this, memory, nodes, edges);
|
|
1647
|
+
}
|
|
1648
|
+
await extractConceptsFromNodes(this, nodes, edges);
|
|
1649
|
+
this.graph = { nodes, edges };
|
|
1650
|
+
if (this.graph.nodes.length > 0 && this.graph.edges.length > 0) {
|
|
1651
|
+
const leidenResult = leiden(this.graph);
|
|
1652
|
+
annotateCommunities(this.graph, leidenResult);
|
|
1653
|
+
}
|
|
1654
|
+
this.initIncrementalPR();
|
|
1655
|
+
this.saveToDisk();
|
|
1656
|
+
return this.graph;
|
|
1657
|
+
}
|
|
1658
|
+
// ─── Hybrid Search ───────────────────────────────────────────────
|
|
1659
|
+
async search(query, graph, topK = 10) {
|
|
1660
|
+
const searchGraph = graph ?? this.graph;
|
|
1661
|
+
if (searchGraph.nodes.length === 0) return [];
|
|
1662
|
+
const queryEmbedding = await this.getEmbedding(query);
|
|
1663
|
+
if (this.pageRankScores.size === 0) {
|
|
1664
|
+
this.computePageRank(searchGraph);
|
|
1665
|
+
}
|
|
1666
|
+
let maxPageRank = 0;
|
|
1667
|
+
for (const node of searchGraph.nodes) {
|
|
1668
|
+
const pr = this.pageRankScores.get(node.id) ?? 0;
|
|
1669
|
+
if (pr > maxPageRank) maxPageRank = pr;
|
|
1670
|
+
}
|
|
1671
|
+
const scored = [];
|
|
1672
|
+
for (const node of searchGraph.nodes) {
|
|
1673
|
+
let cosineSim = 0;
|
|
1674
|
+
if (node.embedding && node.embedding.length > 0) {
|
|
1675
|
+
cosineSim = cosineSimilarity(queryEmbedding, node.embedding);
|
|
1676
|
+
cosineSim = Math.max(0, cosineSim);
|
|
1677
|
+
}
|
|
1678
|
+
const rawPageRank = this.pageRankScores.get(node.id) ?? 0;
|
|
1679
|
+
const normalizedPageRank = maxPageRank > 0 ? rawPageRank / maxPageRank : 0;
|
|
1680
|
+
const textScore = textMatchScore(query, node.content + " " + node.label);
|
|
1681
|
+
const finalScore = ALPHA * cosineSim + BETA * normalizedPageRank + GAMMA * textScore;
|
|
1682
|
+
if (finalScore > 0) {
|
|
1683
|
+
scored.push({ node, score: finalScore });
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1687
|
+
return scored.slice(0, topK).map((s) => s.node);
|
|
1688
|
+
}
|
|
1689
|
+
// ─── Incremental Indexing ─────────────────────────────────────────
|
|
1690
|
+
async indexSession(session) {
|
|
1691
|
+
const edgesBefore = [...this.graph.edges];
|
|
1692
|
+
removeSessionFromGraph(this.graph, session.meta.id);
|
|
1693
|
+
const edgesAfterRemoval = this.graph.edges;
|
|
1694
|
+
const edgesAfterSet = new Set(edgesAfterRemoval);
|
|
1695
|
+
const removedEdges = edgesBefore.filter((e) => !edgesAfterSet.has(e));
|
|
1696
|
+
const edgeCountBeforeAdd = this.graph.edges.length;
|
|
1697
|
+
const sessionNode = await createSessionNode(this, session);
|
|
1698
|
+
this.graph.nodes.push(sessionNode);
|
|
1699
|
+
await indexSessionTurns(this, session, this.graph.nodes, this.graph.edges);
|
|
1700
|
+
const newEdges = this.graph.edges.slice(edgeCountBeforeAdd);
|
|
1701
|
+
if (!this.incrementalPR) {
|
|
1702
|
+
this.initIncrementalPR();
|
|
1703
|
+
} else {
|
|
1704
|
+
this.applyEdgeRemovals(removedEdges);
|
|
1705
|
+
this.applyEdgeAdditions(newEdges);
|
|
1706
|
+
}
|
|
1707
|
+
this.saveToDisk();
|
|
1708
|
+
}
|
|
1709
|
+
async indexMemory(scope, content) {
|
|
1710
|
+
const edgesBefore = [...this.graph.edges];
|
|
1711
|
+
removeMemoryFromGraph(this.graph, scope);
|
|
1712
|
+
const edgesAfterRemoval = this.graph.edges;
|
|
1713
|
+
const edgesAfterSet = new Set(edgesAfterRemoval);
|
|
1714
|
+
const removedEdges = edgesBefore.filter((e) => !edgesAfterSet.has(e));
|
|
1715
|
+
const edgeCountBeforeAdd = this.graph.edges.length;
|
|
1716
|
+
await buildMemoryNodesFromContent(this, scope, content, this.graph.nodes, this.graph.edges);
|
|
1717
|
+
const newEdges = this.graph.edges.slice(edgeCountBeforeAdd);
|
|
1718
|
+
if (!this.incrementalPR) {
|
|
1719
|
+
this.initIncrementalPR();
|
|
1720
|
+
} else {
|
|
1721
|
+
this.applyEdgeRemovals(removedEdges);
|
|
1722
|
+
this.applyEdgeAdditions(newEdges);
|
|
1723
|
+
}
|
|
1724
|
+
this.saveToDisk();
|
|
1725
|
+
}
|
|
1726
|
+
// ─── Pramana Lookup ──────────────────────────────────────────────
|
|
1727
|
+
/**
|
|
1728
|
+
* Look up the dominant pramana type for a given node ID.
|
|
1729
|
+
*
|
|
1730
|
+
* Queries the edges table for edges involving this node (as source or target)
|
|
1731
|
+
* and returns the most common non-null pramana value. Falls back to 'shabda'
|
|
1732
|
+
* (testimony/documentation) if no pramana data is found.
|
|
1733
|
+
*
|
|
1734
|
+
* @param nodeId - The graph node ID to look up.
|
|
1735
|
+
* @returns The dominant PramanaType for this node.
|
|
1736
|
+
*/
|
|
1737
|
+
lookupPramana(nodeId) {
|
|
1738
|
+
const db = this.getGraphDb();
|
|
1739
|
+
if (!db) return "shabda";
|
|
1740
|
+
try {
|
|
1741
|
+
const row = db.prepare(`
|
|
1742
|
+
SELECT pramana, COUNT(*) as cnt
|
|
1743
|
+
FROM edges
|
|
1744
|
+
WHERE (source = ? OR target = ?) AND pramana IS NOT NULL
|
|
1745
|
+
GROUP BY pramana
|
|
1746
|
+
ORDER BY cnt DESC
|
|
1747
|
+
LIMIT 1
|
|
1748
|
+
`).get(nodeId, nodeId);
|
|
1749
|
+
if (row?.pramana) return row.pramana;
|
|
1750
|
+
} catch {
|
|
1751
|
+
}
|
|
1752
|
+
return "shabda";
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Batch look up pramana types for multiple node IDs.
|
|
1756
|
+
*
|
|
1757
|
+
* More efficient than calling lookupPramana() individually for each node.
|
|
1758
|
+
* Returns a Map from node ID to PramanaType. Missing entries default to 'shabda'.
|
|
1759
|
+
*
|
|
1760
|
+
* @param nodeIds - Array of node IDs to look up.
|
|
1761
|
+
* @returns Map from node ID to its dominant PramanaType.
|
|
1762
|
+
*/
|
|
1763
|
+
lookupPramanaBatch(nodeIds) {
|
|
1764
|
+
const result = /* @__PURE__ */ new Map();
|
|
1765
|
+
if (nodeIds.length === 0) return result;
|
|
1766
|
+
const db = this.getGraphDb();
|
|
1767
|
+
if (!db) {
|
|
1768
|
+
for (const id of nodeIds) result.set(id, "shabda");
|
|
1769
|
+
return result;
|
|
1770
|
+
}
|
|
1771
|
+
try {
|
|
1772
|
+
const placeholders = nodeIds.map(() => "?").join(",");
|
|
1773
|
+
const rows = db.prepare(`
|
|
1774
|
+
SELECT node_id, pramana FROM (
|
|
1775
|
+
SELECT source AS node_id, pramana, COUNT(*) AS cnt
|
|
1776
|
+
FROM edges
|
|
1777
|
+
WHERE source IN (${placeholders}) AND pramana IS NOT NULL
|
|
1778
|
+
GROUP BY source, pramana
|
|
1779
|
+
UNION ALL
|
|
1780
|
+
SELECT target AS node_id, pramana, COUNT(*) AS cnt
|
|
1781
|
+
FROM edges
|
|
1782
|
+
WHERE target IN (${placeholders}) AND pramana IS NOT NULL
|
|
1783
|
+
GROUP BY target, pramana
|
|
1784
|
+
)
|
|
1785
|
+
GROUP BY node_id
|
|
1786
|
+
HAVING cnt = MAX(cnt)
|
|
1787
|
+
`).all(...nodeIds, ...nodeIds);
|
|
1788
|
+
for (const row of rows) {
|
|
1789
|
+
result.set(row.node_id, row.pramana);
|
|
1790
|
+
}
|
|
1791
|
+
} catch {
|
|
1792
|
+
}
|
|
1793
|
+
for (const id of nodeIds) {
|
|
1794
|
+
if (!result.has(id)) result.set(id, "shabda");
|
|
1795
|
+
}
|
|
1796
|
+
return result;
|
|
1797
|
+
}
|
|
1798
|
+
// ─── SQLite Persistence Helpers ─────────────────────────────────
|
|
1799
|
+
/**
|
|
1800
|
+
* Get the graph database handle, initializing the schema on first access.
|
|
1801
|
+
* Returns null if the database layer is unavailable (e.g. mocked fs in tests).
|
|
1802
|
+
*/
|
|
1803
|
+
getGraphDb() {
|
|
1804
|
+
try {
|
|
1805
|
+
const dbm = DatabaseManager.instance();
|
|
1806
|
+
if (!this._graphDbInitialized) {
|
|
1807
|
+
initGraphSchema(dbm);
|
|
1808
|
+
this._graphDbInitialized = true;
|
|
1809
|
+
}
|
|
1810
|
+
return dbm.get("graph");
|
|
1811
|
+
} catch {
|
|
1812
|
+
return null;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
/** Insert or replace a node in the SQLite nodes table. */
|
|
1816
|
+
upsertNode(db, node) {
|
|
1817
|
+
db.prepare(`
|
|
1818
|
+
INSERT OR REPLACE INTO nodes (id, type, label, content, metadata, created_at, updated_at)
|
|
1819
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1820
|
+
`).run(
|
|
1821
|
+
node.id,
|
|
1822
|
+
node.type,
|
|
1823
|
+
node.label,
|
|
1824
|
+
node.content,
|
|
1825
|
+
JSON.stringify(node.metadata),
|
|
1826
|
+
Date.now(),
|
|
1827
|
+
Date.now()
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
/** Insert an edge into the SQLite edges table (bi-temporal fields mapped). */
|
|
1831
|
+
insertEdge(db, edge) {
|
|
1832
|
+
db.prepare(`
|
|
1833
|
+
INSERT OR IGNORE INTO edges (source, target, relationship, weight, pramana, viveka,
|
|
1834
|
+
valid_from, valid_until, recorded_at, superseded_at)
|
|
1835
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1836
|
+
`).run(
|
|
1837
|
+
edge.source,
|
|
1838
|
+
edge.target,
|
|
1839
|
+
edge.relationship,
|
|
1840
|
+
edge.weight,
|
|
1841
|
+
null,
|
|
1842
|
+
// pramana — not yet used
|
|
1843
|
+
null,
|
|
1844
|
+
// viveka — not yet used
|
|
1845
|
+
edge.validFrom ? new Date(edge.validFrom).getTime() : Date.now(),
|
|
1846
|
+
edge.validUntil ? new Date(edge.validUntil).getTime() : null,
|
|
1847
|
+
edge.recordedAt ? new Date(edge.recordedAt).getTime() : Date.now(),
|
|
1848
|
+
edge.supersededAt ? new Date(edge.supersededAt).getTime() : null
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
// ─── Persistence ──────────────────────────────────────────────────
|
|
1852
|
+
saveToDisk() {
|
|
1853
|
+
try {
|
|
1854
|
+
const db = this.getGraphDb();
|
|
1855
|
+
if (db) {
|
|
1856
|
+
db.transaction(() => {
|
|
1857
|
+
db.prepare("DELETE FROM pagerank").run();
|
|
1858
|
+
db.prepare("DELETE FROM edges").run();
|
|
1859
|
+
db.prepare("DELETE FROM nodes").run();
|
|
1860
|
+
for (const node of this.graph.nodes) {
|
|
1861
|
+
this.upsertNode(db, node);
|
|
1862
|
+
}
|
|
1863
|
+
for (const edge of this.graph.edges) {
|
|
1864
|
+
this.insertEdge(db, edge);
|
|
1865
|
+
}
|
|
1866
|
+
for (const [nodeId, score] of this.pageRankScores) {
|
|
1867
|
+
db.prepare(
|
|
1868
|
+
"INSERT OR REPLACE INTO pagerank (node_id, score, updated_at) VALUES (?, ?, ?)"
|
|
1869
|
+
).run(nodeId, score, Date.now());
|
|
1870
|
+
}
|
|
1871
|
+
})();
|
|
1872
|
+
this.saveEmbeddingsJson();
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
} catch {
|
|
1876
|
+
}
|
|
1877
|
+
this.saveToDiskJson();
|
|
1878
|
+
}
|
|
1879
|
+
loadFromDisk() {
|
|
1880
|
+
let loadedFromSqlite = false;
|
|
1881
|
+
try {
|
|
1882
|
+
const db = this.getGraphDb();
|
|
1883
|
+
if (db) {
|
|
1884
|
+
const nodeCount = db.prepare("SELECT COUNT(*) as cnt FROM nodes").get().cnt;
|
|
1885
|
+
if (nodeCount > 0) {
|
|
1886
|
+
const nodeRows = db.prepare("SELECT * FROM nodes").all();
|
|
1887
|
+
this.graph.nodes = nodeRows.map((row) => ({
|
|
1888
|
+
id: row.id,
|
|
1889
|
+
type: row.type,
|
|
1890
|
+
label: row.label,
|
|
1891
|
+
content: row.content ?? "",
|
|
1892
|
+
metadata: (() => {
|
|
1893
|
+
try {
|
|
1894
|
+
return JSON.parse(row.metadata ?? "{}");
|
|
1895
|
+
} catch {
|
|
1896
|
+
return {};
|
|
1897
|
+
}
|
|
1898
|
+
})()
|
|
1899
|
+
// Note: embedding not stored in nodes table — it's in vectors.db (Phase 0.6)
|
|
1900
|
+
}));
|
|
1901
|
+
const edgeRows = db.prepare("SELECT * FROM edges").all();
|
|
1902
|
+
this.graph.edges = edgeRows.map((row) => ({
|
|
1903
|
+
source: row.source,
|
|
1904
|
+
target: row.target,
|
|
1905
|
+
relationship: row.relationship,
|
|
1906
|
+
weight: row.weight,
|
|
1907
|
+
validFrom: row.valid_from ? new Date(row.valid_from).toISOString() : void 0,
|
|
1908
|
+
validUntil: row.valid_until ? new Date(row.valid_until).toISOString() : void 0,
|
|
1909
|
+
recordedAt: row.recorded_at ? new Date(row.recorded_at).toISOString() : void 0,
|
|
1910
|
+
supersededAt: row.superseded_at ? new Date(row.superseded_at).toISOString() : void 0
|
|
1911
|
+
}));
|
|
1912
|
+
const prRows = db.prepare("SELECT * FROM pagerank").all();
|
|
1913
|
+
for (const row of prRows) {
|
|
1914
|
+
this.pageRankScores.set(row.node_id, row.score);
|
|
1915
|
+
}
|
|
1916
|
+
loadedFromSqlite = true;
|
|
1917
|
+
} else {
|
|
1918
|
+
this.loadFromDiskJson();
|
|
1919
|
+
if (this.graph.nodes.length > 0) {
|
|
1920
|
+
try {
|
|
1921
|
+
db.transaction(() => {
|
|
1922
|
+
for (const node of this.graph.nodes) {
|
|
1923
|
+
this.upsertNode(db, node);
|
|
1924
|
+
}
|
|
1925
|
+
for (const edge of this.graph.edges) {
|
|
1926
|
+
this.insertEdge(db, edge);
|
|
1927
|
+
}
|
|
1928
|
+
for (const [nodeId, score] of this.pageRankScores) {
|
|
1929
|
+
db.prepare(
|
|
1930
|
+
"INSERT OR REPLACE INTO pagerank (node_id, score, updated_at) VALUES (?, ?, ?)"
|
|
1931
|
+
).run(nodeId, score, Date.now());
|
|
1932
|
+
}
|
|
1933
|
+
})();
|
|
1934
|
+
} catch {
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
loadedFromSqlite = true;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
} catch {
|
|
1941
|
+
}
|
|
1942
|
+
if (!loadedFromSqlite) {
|
|
1943
|
+
this.loadFromDiskJson();
|
|
1944
|
+
}
|
|
1945
|
+
this.loadEmbeddingsJson();
|
|
1946
|
+
if (this.graph.edges.length > 0) {
|
|
1947
|
+
this.initIncrementalPR();
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
// ─── JSON Fallback Persistence (legacy / migration) ───────────────
|
|
1951
|
+
saveToDiskJson() {
|
|
1952
|
+
try {
|
|
1953
|
+
const dir = getGraphDir();
|
|
1954
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1955
|
+
fs.writeFileSync(getGraphPath(), JSON.stringify(this.graph, null, " "), "utf-8");
|
|
1956
|
+
this.saveEmbeddingsJson();
|
|
1957
|
+
const pageRankObj = {};
|
|
1958
|
+
for (const [key, value] of this.pageRankScores) {
|
|
1959
|
+
pageRankObj[key] = value;
|
|
1960
|
+
}
|
|
1961
|
+
fs.writeFileSync(getPageRankPath(), JSON.stringify(pageRankObj, null, " "), "utf-8");
|
|
1962
|
+
} catch {
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
saveEmbeddingsJson() {
|
|
1966
|
+
try {
|
|
1967
|
+
const dir = getGraphDir();
|
|
1968
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1969
|
+
const embeddings = {};
|
|
1970
|
+
for (const [key, value] of this.embeddingCache) {
|
|
1971
|
+
embeddings[key] = value;
|
|
1972
|
+
}
|
|
1973
|
+
fs.writeFileSync(getEmbeddingsPath(), JSON.stringify(embeddings, null, " "), "utf-8");
|
|
1974
|
+
} catch {
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
loadFromDiskJson() {
|
|
1978
|
+
try {
|
|
1979
|
+
const graphPath = getGraphPath();
|
|
1980
|
+
if (fs.existsSync(graphPath)) {
|
|
1981
|
+
const raw = fs.readFileSync(graphPath, "utf-8");
|
|
1982
|
+
this.graph = JSON.parse(raw);
|
|
1983
|
+
}
|
|
1984
|
+
} catch {
|
|
1985
|
+
this.graph = { nodes: [], edges: [] };
|
|
1986
|
+
}
|
|
1987
|
+
try {
|
|
1988
|
+
const prPath = getPageRankPath();
|
|
1989
|
+
if (fs.existsSync(prPath)) {
|
|
1990
|
+
const raw = fs.readFileSync(prPath, "utf-8");
|
|
1991
|
+
const ranks = JSON.parse(raw);
|
|
1992
|
+
for (const [key, value] of Object.entries(ranks)) {
|
|
1993
|
+
this.pageRankScores.set(key, value);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
} catch {
|
|
1997
|
+
this.pageRankScores = /* @__PURE__ */ new Map();
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
loadEmbeddingsJson() {
|
|
2001
|
+
try {
|
|
2002
|
+
const embPath = getEmbeddingsPath();
|
|
2003
|
+
if (fs.existsSync(embPath)) {
|
|
2004
|
+
const raw = fs.readFileSync(embPath, "utf-8");
|
|
2005
|
+
const embeddings = JSON.parse(raw);
|
|
2006
|
+
for (const [key, value] of Object.entries(embeddings)) {
|
|
2007
|
+
this.embeddingCache.set(key, value);
|
|
2008
|
+
}
|
|
2009
|
+
while (this.embeddingCache.size > this.maxEmbeddingCacheSize) {
|
|
2010
|
+
const oldest = this.embeddingCache.keys().next().value;
|
|
2011
|
+
if (oldest !== void 0) {
|
|
2012
|
+
this.embeddingCache.delete(oldest);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
} catch {
|
|
2017
|
+
this.embeddingCache = /* @__PURE__ */ new Map();
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
getGraph() {
|
|
2021
|
+
return this.graph;
|
|
2022
|
+
}
|
|
2023
|
+
async clear() {
|
|
2024
|
+
this.graph = { nodes: [], edges: [] };
|
|
2025
|
+
this.embeddingCache = /* @__PURE__ */ new Map();
|
|
2026
|
+
this.pageRankScores = /* @__PURE__ */ new Map();
|
|
2027
|
+
this.incrementalPR = null;
|
|
2028
|
+
try {
|
|
2029
|
+
const db = this.getGraphDb();
|
|
2030
|
+
if (db) {
|
|
2031
|
+
db.exec("DELETE FROM pagerank; DELETE FROM edges; DELETE FROM nodes;");
|
|
2032
|
+
}
|
|
2033
|
+
} catch {
|
|
2034
|
+
}
|
|
2035
|
+
this.saveToDisk();
|
|
2036
|
+
}
|
|
2037
|
+
resetAvailability() {
|
|
2038
|
+
this.ollamaAvailable = null;
|
|
2039
|
+
this.embeddingService.resetAvailability();
|
|
2040
|
+
}
|
|
2041
|
+
// ─── Graph Neighbor Queries ───────────────────────────────────────
|
|
2042
|
+
/**
|
|
2043
|
+
* Get edges connected to a node, optionally filtered by direction.
|
|
2044
|
+
* Uses the in-memory graph for fast access.
|
|
2045
|
+
*/
|
|
2046
|
+
getNeighbors(nodeId, direction = "both") {
|
|
2047
|
+
return this.graph.edges.filter((edge) => {
|
|
2048
|
+
if (direction === "out") return edge.source === nodeId;
|
|
2049
|
+
if (direction === "in") return edge.target === nodeId;
|
|
2050
|
+
return edge.source === nodeId || edge.target === nodeId;
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
function migrateGraphJson() {
|
|
2055
|
+
const graphPath = getGraphPath();
|
|
2056
|
+
const prPath = getPageRankPath();
|
|
2057
|
+
if (!fs.existsSync(graphPath)) {
|
|
2058
|
+
return { nodes: 0, edges: 0 };
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const raw = fs.readFileSync(graphPath, "utf-8");
|
|
2062
|
+
const graph = JSON.parse(raw);
|
|
2063
|
+
if (!graph.nodes || graph.nodes.length === 0) {
|
|
2064
|
+
return { nodes: 0, edges: 0 };
|
|
2065
|
+
}
|
|
2066
|
+
const pageRankScores = /* @__PURE__ */ new Map();
|
|
2067
|
+
try {
|
|
2068
|
+
if (fs.existsSync(prPath)) {
|
|
2069
|
+
const prRaw = fs.readFileSync(prPath, "utf-8");
|
|
2070
|
+
const ranks = JSON.parse(prRaw);
|
|
2071
|
+
for (const [key, value] of Object.entries(ranks)) {
|
|
2072
|
+
pageRankScores.set(key, value);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
} catch {
|
|
2076
|
+
}
|
|
2077
|
+
const dbm = DatabaseManager.instance();
|
|
2078
|
+
initGraphSchema(dbm);
|
|
2079
|
+
const db = dbm.get("graph");
|
|
2080
|
+
db.transaction(() => {
|
|
2081
|
+
const upsertNodeStmt = db.prepare(`
|
|
2082
|
+
INSERT OR REPLACE INTO nodes (id, type, label, content, metadata, created_at, updated_at)
|
|
2083
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2084
|
+
`);
|
|
2085
|
+
const insertEdgeStmt = db.prepare(`
|
|
2086
|
+
INSERT OR IGNORE INTO edges (source, target, relationship, weight, pramana, viveka,
|
|
2087
|
+
valid_from, valid_until, recorded_at, superseded_at)
|
|
2088
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2089
|
+
`);
|
|
2090
|
+
const upsertPRStmt = db.prepare(
|
|
2091
|
+
"INSERT OR REPLACE INTO pagerank (node_id, score, updated_at) VALUES (?, ?, ?)"
|
|
2092
|
+
);
|
|
2093
|
+
const now = Date.now();
|
|
2094
|
+
for (const node of graph.nodes) {
|
|
2095
|
+
upsertNodeStmt.run(
|
|
2096
|
+
node.id,
|
|
2097
|
+
node.type,
|
|
2098
|
+
node.label,
|
|
2099
|
+
node.content,
|
|
2100
|
+
JSON.stringify(node.metadata),
|
|
2101
|
+
now,
|
|
2102
|
+
now
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
for (const edge of graph.edges) {
|
|
2106
|
+
insertEdgeStmt.run(
|
|
2107
|
+
edge.source,
|
|
2108
|
+
edge.target,
|
|
2109
|
+
edge.relationship,
|
|
2110
|
+
edge.weight,
|
|
2111
|
+
null,
|
|
2112
|
+
null,
|
|
2113
|
+
edge.validFrom ? new Date(edge.validFrom).getTime() : now,
|
|
2114
|
+
edge.validUntil ? new Date(edge.validUntil).getTime() : null,
|
|
2115
|
+
edge.recordedAt ? new Date(edge.recordedAt).getTime() : now,
|
|
2116
|
+
edge.supersededAt ? new Date(edge.supersededAt).getTime() : null
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
for (const [nodeId, score] of pageRankScores) {
|
|
2120
|
+
upsertPRStmt.run(nodeId, score, now);
|
|
2121
|
+
}
|
|
2122
|
+
})();
|
|
2123
|
+
try {
|
|
2124
|
+
fs.renameSync(graphPath, graphPath + ".bak");
|
|
2125
|
+
} catch {
|
|
2126
|
+
}
|
|
2127
|
+
try {
|
|
2128
|
+
if (fs.existsSync(prPath)) {
|
|
2129
|
+
fs.renameSync(prPath, prPath + ".bak");
|
|
2130
|
+
}
|
|
2131
|
+
} catch {
|
|
2132
|
+
}
|
|
2133
|
+
return { nodes: graph.nodes.length, edges: graph.edges.length };
|
|
2134
|
+
} catch {
|
|
2135
|
+
return { nodes: 0, edges: 0 };
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
export {
|
|
2140
|
+
computePersonalizedPageRank,
|
|
2141
|
+
IncrementalPageRank,
|
|
2142
|
+
NERExtractor,
|
|
2143
|
+
createEdge,
|
|
2144
|
+
supersedEdge,
|
|
2145
|
+
expireEdge,
|
|
2146
|
+
queryEdgesAtTime,
|
|
2147
|
+
getEdgeHistory,
|
|
2148
|
+
temporalDecay,
|
|
2149
|
+
compactEdges,
|
|
2150
|
+
leiden,
|
|
2151
|
+
annotateCommunities,
|
|
2152
|
+
communitySummary,
|
|
2153
|
+
filterByCommunity,
|
|
2154
|
+
findBridgeNodes,
|
|
2155
|
+
GraphRAGEngine,
|
|
2156
|
+
migrateGraphJson
|
|
2157
|
+
};
|
|
2158
|
+
//# sourceMappingURL=chunk-S4TBVCL2.js.map
|