@omiron33/omi-neuron-web 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/README.md +55 -0
- package/dist/api/index.cjs +943 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.d.cts +140 -0
- package/dist/api/index.d.ts +140 -0
- package/dist/api/index.js +934 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-BSOSHBDR.cjs +300 -0
- package/dist/chunk-BSOSHBDR.cjs.map +1 -0
- package/dist/chunk-COO66N7H.cjs +950 -0
- package/dist/chunk-COO66N7H.cjs.map +1 -0
- package/dist/chunk-FXKXMSLY.cjs +270 -0
- package/dist/chunk-FXKXMSLY.cjs.map +1 -0
- package/dist/chunk-PSDVPB7Y.js +289 -0
- package/dist/chunk-PSDVPB7Y.js.map +1 -0
- package/dist/chunk-RQCGONPN.js +937 -0
- package/dist/chunk-RQCGONPN.js.map +1 -0
- package/dist/chunk-RTSFO7BW.cjs +592 -0
- package/dist/chunk-RTSFO7BW.cjs.map +1 -0
- package/dist/chunk-TFLMPBX7.js +262 -0
- package/dist/chunk-TFLMPBX7.js.map +1 -0
- package/dist/chunk-XNR42GCJ.js +547 -0
- package/dist/chunk-XNR42GCJ.js.map +1 -0
- package/dist/cli/index.cjs +571 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +563 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/database-B0vplyA4.d.cts +41 -0
- package/dist/database-B0vplyA4.d.ts +41 -0
- package/dist/edge-BzsYe2Ed.d.cts +269 -0
- package/dist/edge-BzsYe2Ed.d.ts +269 -0
- package/dist/index.cjs +895 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1484 -0
- package/dist/index.d.ts +1484 -0
- package/dist/index.js +654 -0
- package/dist/index.js.map +1 -0
- package/dist/migration/index.cjs +32 -0
- package/dist/migration/index.cjs.map +1 -0
- package/dist/migration/index.d.cts +51 -0
- package/dist/migration/index.d.ts +51 -0
- package/dist/migration/index.js +3 -0
- package/dist/migration/index.js.map +1 -0
- package/dist/query-helpers-D8po5Mn-.d.cts +777 -0
- package/dist/query-helpers-DvQTA2_Z.d.ts +777 -0
- package/dist/visualization/index.cjs +485 -0
- package/dist/visualization/index.cjs.map +1 -0
- package/dist/visualization/index.d.cts +134 -0
- package/dist/visualization/index.d.ts +134 -0
- package/dist/visualization/index.js +460 -0
- package/dist/visualization/index.js.map +1 -0
- package/docker/docker-compose.template.yml +28 -0
- package/package.json +116 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { Pool } from 'pg';
|
|
4
|
+
|
|
5
|
+
// src/core/events/event-bus.ts
|
|
6
|
+
var EventBus = class {
|
|
7
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
8
|
+
globalSubscribers = /* @__PURE__ */ new Set();
|
|
9
|
+
middleware = [];
|
|
10
|
+
history = [];
|
|
11
|
+
historyLimit = 200;
|
|
12
|
+
emit(event) {
|
|
13
|
+
void this.emitAsync(event);
|
|
14
|
+
}
|
|
15
|
+
async emitAsync(event) {
|
|
16
|
+
await this.runMiddleware(event, async () => {
|
|
17
|
+
this.recordEvent(event);
|
|
18
|
+
const handlers = this.subscribers.get(event.type);
|
|
19
|
+
if (handlers) {
|
|
20
|
+
for (const handler of handlers) {
|
|
21
|
+
await handler(event);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const handler of this.globalSubscribers) {
|
|
25
|
+
await handler(event);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
subscribe(type, handler) {
|
|
30
|
+
if (!this.subscribers.has(type)) {
|
|
31
|
+
this.subscribers.set(type, /* @__PURE__ */ new Set());
|
|
32
|
+
}
|
|
33
|
+
this.subscribers.get(type).add(handler);
|
|
34
|
+
return {
|
|
35
|
+
unsubscribe: () => this.unsubscribe(type, handler)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
subscribeMany(types, handler) {
|
|
39
|
+
types.forEach((type) => this.subscribe(type, handler));
|
|
40
|
+
return {
|
|
41
|
+
unsubscribe: () => types.forEach((type) => this.unsubscribe(type, handler))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
subscribeAll(handler) {
|
|
45
|
+
this.globalSubscribers.add(handler);
|
|
46
|
+
return {
|
|
47
|
+
unsubscribe: () => {
|
|
48
|
+
this.globalSubscribers.delete(handler);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
unsubscribe(type, handler) {
|
|
53
|
+
const handlers = this.subscribers.get(type);
|
|
54
|
+
if (!handlers) return;
|
|
55
|
+
handlers.delete(handler);
|
|
56
|
+
if (handlers.size === 0) {
|
|
57
|
+
this.subscribers.delete(type);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
unsubscribeAll() {
|
|
61
|
+
this.subscribers.clear();
|
|
62
|
+
this.globalSubscribers.clear();
|
|
63
|
+
}
|
|
64
|
+
use(middleware) {
|
|
65
|
+
this.middleware.push(middleware);
|
|
66
|
+
}
|
|
67
|
+
getSubscriberCount(type) {
|
|
68
|
+
if (!type) {
|
|
69
|
+
let count = this.globalSubscribers.size;
|
|
70
|
+
for (const handlers of this.subscribers.values()) {
|
|
71
|
+
count += handlers.size;
|
|
72
|
+
}
|
|
73
|
+
return count;
|
|
74
|
+
}
|
|
75
|
+
return this.subscribers.get(type)?.size ?? 0;
|
|
76
|
+
}
|
|
77
|
+
getEventHistory(limit) {
|
|
78
|
+
if (!limit) return [...this.history];
|
|
79
|
+
return this.history.slice(-limit);
|
|
80
|
+
}
|
|
81
|
+
clearHistory() {
|
|
82
|
+
this.history = [];
|
|
83
|
+
}
|
|
84
|
+
recordEvent(event) {
|
|
85
|
+
this.history.push(event);
|
|
86
|
+
if (this.history.length > this.historyLimit) {
|
|
87
|
+
this.history.shift();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async runMiddleware(event, finalHandler) {
|
|
91
|
+
const stack = [...this.middleware];
|
|
92
|
+
let index = -1;
|
|
93
|
+
const runner = async () => {
|
|
94
|
+
index += 1;
|
|
95
|
+
if (index < stack.length) {
|
|
96
|
+
await stack[index](event, runner);
|
|
97
|
+
} else {
|
|
98
|
+
await finalHandler();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
await runner();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var createEvent = (type, payload, source = "system") => ({
|
|
105
|
+
type,
|
|
106
|
+
payload,
|
|
107
|
+
source,
|
|
108
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
109
|
+
});
|
|
110
|
+
var EmbeddingsService = class {
|
|
111
|
+
constructor(config, db) {
|
|
112
|
+
this.config = config;
|
|
113
|
+
this.db = db;
|
|
114
|
+
this.client = new OpenAI({ apiKey: config.openaiApiKey });
|
|
115
|
+
}
|
|
116
|
+
client;
|
|
117
|
+
async generateEmbedding(text) {
|
|
118
|
+
const response = await this.client.embeddings.create({
|
|
119
|
+
model: this.config.model,
|
|
120
|
+
input: text,
|
|
121
|
+
dimensions: this.config.dimensions
|
|
122
|
+
});
|
|
123
|
+
return response.data[0].embedding;
|
|
124
|
+
}
|
|
125
|
+
async generateBatchEmbeddings(texts) {
|
|
126
|
+
const response = await this.client.embeddings.create({
|
|
127
|
+
model: this.config.model,
|
|
128
|
+
input: texts,
|
|
129
|
+
dimensions: this.config.dimensions
|
|
130
|
+
});
|
|
131
|
+
return response.data.map((item) => item.embedding);
|
|
132
|
+
}
|
|
133
|
+
async embedNode(nodeId) {
|
|
134
|
+
const node = await this.db.queryOne(
|
|
135
|
+
"SELECT id, content, embedding, embedding_generated_at FROM nodes WHERE id = $1",
|
|
136
|
+
[nodeId]
|
|
137
|
+
);
|
|
138
|
+
if (!node) {
|
|
139
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
140
|
+
}
|
|
141
|
+
const cached = await this.getCachedEmbedding(nodeId);
|
|
142
|
+
if (cached) {
|
|
143
|
+
return {
|
|
144
|
+
nodeId,
|
|
145
|
+
embedding: cached,
|
|
146
|
+
model: this.config.model,
|
|
147
|
+
tokenCount: this.countTokens(node.content ?? ""),
|
|
148
|
+
cached: true
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const text = node.content ?? "";
|
|
152
|
+
const embedding = await this.withRetry(() => this.generateEmbedding(text));
|
|
153
|
+
await this.cacheEmbedding(nodeId, embedding, this.config.model);
|
|
154
|
+
return {
|
|
155
|
+
nodeId,
|
|
156
|
+
embedding,
|
|
157
|
+
model: this.config.model,
|
|
158
|
+
tokenCount: this.countTokens(text),
|
|
159
|
+
cached: false
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async embedNodes(nodeIds) {
|
|
163
|
+
const results = [];
|
|
164
|
+
const errors = [];
|
|
165
|
+
const batches = [];
|
|
166
|
+
for (let i = 0; i < nodeIds.length; i += this.config.batchSize) {
|
|
167
|
+
batches.push(nodeIds.slice(i, i + this.config.batchSize));
|
|
168
|
+
}
|
|
169
|
+
for (const batch of batches) {
|
|
170
|
+
try {
|
|
171
|
+
const nodes = await this.db.query(
|
|
172
|
+
"SELECT id, content FROM nodes WHERE id = ANY($1)",
|
|
173
|
+
[batch]
|
|
174
|
+
);
|
|
175
|
+
const texts = nodes.map((node) => node.content ?? "");
|
|
176
|
+
const embeddings = await this.withRetry(() => this.generateBatchEmbeddings(texts));
|
|
177
|
+
for (let idx = 0; idx < nodes.length; idx += 1) {
|
|
178
|
+
const node = nodes[idx];
|
|
179
|
+
const embedding = embeddings[idx];
|
|
180
|
+
await this.cacheEmbedding(node.id, embedding, this.config.model);
|
|
181
|
+
results.push({
|
|
182
|
+
nodeId: node.id,
|
|
183
|
+
embedding,
|
|
184
|
+
model: this.config.model,
|
|
185
|
+
tokenCount: this.countTokens(node.content ?? ""),
|
|
186
|
+
cached: false
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
await this.applyRateLimit();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
batch.forEach((nodeId) => {
|
|
192
|
+
errors.push({ nodeId, error: error instanceof Error ? error.message : String(error) });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { results, errors };
|
|
197
|
+
}
|
|
198
|
+
async getCachedEmbedding(nodeId) {
|
|
199
|
+
const row = await this.db.queryOne(
|
|
200
|
+
"SELECT embedding, embedding_generated_at, embedding_model FROM nodes WHERE id = $1",
|
|
201
|
+
[nodeId]
|
|
202
|
+
);
|
|
203
|
+
if (!row?.embedding) return null;
|
|
204
|
+
if (row.embedding_model !== this.config.model) return null;
|
|
205
|
+
if (!row.embedding_generated_at) return row.embedding;
|
|
206
|
+
const ageSeconds = (Date.now() - row.embedding_generated_at.getTime()) / 1e3;
|
|
207
|
+
if (ageSeconds > this.config.cacheTTL) return null;
|
|
208
|
+
return row.embedding;
|
|
209
|
+
}
|
|
210
|
+
async cacheEmbedding(nodeId, embedding, model) {
|
|
211
|
+
await this.db.execute(
|
|
212
|
+
"UPDATE nodes SET embedding = $1, embedding_model = $2, embedding_generated_at = NOW() WHERE id = $3",
|
|
213
|
+
[embedding, model, nodeId]
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
async invalidateCache(nodeIds) {
|
|
217
|
+
if (nodeIds?.length) {
|
|
218
|
+
await this.db.execute(
|
|
219
|
+
"UPDATE nodes SET embedding = NULL, embedding_model = NULL, embedding_generated_at = NULL WHERE id = ANY($1)",
|
|
220
|
+
[nodeIds]
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await this.db.execute(
|
|
225
|
+
"UPDATE nodes SET embedding = NULL, embedding_model = NULL, embedding_generated_at = NULL"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
countTokens(text) {
|
|
229
|
+
return Math.ceil(text.length / 4);
|
|
230
|
+
}
|
|
231
|
+
estimateCost(nodeCount) {
|
|
232
|
+
const tokens = nodeCount * 512;
|
|
233
|
+
const pricePerMillion = this.config.model === "text-embedding-3-large" ? 0.13 : this.config.model === "text-embedding-3-small" ? 0.02 : 0.1;
|
|
234
|
+
const cost = tokens / 1e6 * pricePerMillion;
|
|
235
|
+
return { tokens, cost };
|
|
236
|
+
}
|
|
237
|
+
async withRetry(fn) {
|
|
238
|
+
let attempt = 0;
|
|
239
|
+
while (attempt <= this.config.maxRetries) {
|
|
240
|
+
try {
|
|
241
|
+
return await fn();
|
|
242
|
+
} catch (error) {
|
|
243
|
+
attempt += 1;
|
|
244
|
+
if (attempt > this.config.maxRetries) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
const delay = 500 * Math.pow(2, attempt);
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
throw new Error("Exceeded retry attempts");
|
|
252
|
+
}
|
|
253
|
+
async applyRateLimit() {
|
|
254
|
+
const delayMs = Math.ceil(6e4 / this.config.rateLimit);
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var cosineSimilarity = (a, b) => {
|
|
259
|
+
let dot = 0;
|
|
260
|
+
let magA = 0;
|
|
261
|
+
let magB = 0;
|
|
262
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
263
|
+
dot += a[i] * b[i];
|
|
264
|
+
magA += a[i] * a[i];
|
|
265
|
+
magB += b[i] * b[i];
|
|
266
|
+
}
|
|
267
|
+
if (magA === 0 || magB === 0) return 0;
|
|
268
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
269
|
+
};
|
|
270
|
+
var averageVector = (vectors) => {
|
|
271
|
+
const length = vectors[0]?.length ?? 0;
|
|
272
|
+
const sum = new Array(length).fill(0);
|
|
273
|
+
vectors.forEach((vector) => {
|
|
274
|
+
vector.forEach((value, idx) => {
|
|
275
|
+
sum[idx] += value;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
return sum.map((value) => value / vectors.length);
|
|
279
|
+
};
|
|
280
|
+
var ClusteringEngine = class {
|
|
281
|
+
constructor(db, embeddings) {
|
|
282
|
+
this.db = db;
|
|
283
|
+
this.embeddings = embeddings;
|
|
284
|
+
}
|
|
285
|
+
async clusterNodes(config) {
|
|
286
|
+
const nodes = await this.fetchNodesWithEmbeddings();
|
|
287
|
+
if (nodes.length === 0) return { clusters: [], unassigned: [] };
|
|
288
|
+
let clusters = [];
|
|
289
|
+
if (config.algorithm === "dbscan") {
|
|
290
|
+
clusters = this.runDbscan(nodes, config);
|
|
291
|
+
} else {
|
|
292
|
+
clusters = this.runKmeans(nodes, config);
|
|
293
|
+
}
|
|
294
|
+
await this.persistClusters(clusters);
|
|
295
|
+
const assigned = new Set(clusters.flatMap((cluster) => cluster.nodeIds));
|
|
296
|
+
const unassigned = nodes.map((node) => node.id).filter((id) => !assigned.has(id));
|
|
297
|
+
return { clusters, unassigned };
|
|
298
|
+
}
|
|
299
|
+
async recluster(config) {
|
|
300
|
+
const { clusters } = await this.clusterNodes(config);
|
|
301
|
+
return clusters;
|
|
302
|
+
}
|
|
303
|
+
async assignToCluster(nodeId) {
|
|
304
|
+
const node = await this.db.queryOne(
|
|
305
|
+
"SELECT id, embedding FROM nodes WHERE id = $1",
|
|
306
|
+
[nodeId]
|
|
307
|
+
);
|
|
308
|
+
if (!node?.embedding) return null;
|
|
309
|
+
const best = await this.findBestCluster(node.embedding);
|
|
310
|
+
if (!best) return null;
|
|
311
|
+
await this.db.execute(
|
|
312
|
+
"INSERT INTO cluster_memberships (node_id, cluster_id, similarity_score, is_primary) VALUES ($1, $2, $3, true) ON CONFLICT (node_id, cluster_id) DO UPDATE SET similarity_score = $3, is_primary = true",
|
|
313
|
+
[nodeId, best.clusterId, best.similarity]
|
|
314
|
+
);
|
|
315
|
+
await this.db.execute("UPDATE nodes SET cluster_id = $1, cluster_similarity = $2 WHERE id = $3", [
|
|
316
|
+
best.clusterId,
|
|
317
|
+
best.similarity,
|
|
318
|
+
nodeId
|
|
319
|
+
]);
|
|
320
|
+
return {
|
|
321
|
+
nodeId,
|
|
322
|
+
clusterId: best.clusterId,
|
|
323
|
+
similarityScore: best.similarity,
|
|
324
|
+
isPrimary: true,
|
|
325
|
+
assignedAt: /* @__PURE__ */ new Date()
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async findBestCluster(embedding) {
|
|
329
|
+
const clusters = await this.db.query(
|
|
330
|
+
"SELECT id, centroid FROM clusters WHERE centroid IS NOT NULL"
|
|
331
|
+
);
|
|
332
|
+
let best = null;
|
|
333
|
+
clusters.forEach((cluster) => {
|
|
334
|
+
if (!cluster.centroid) return;
|
|
335
|
+
const similarity = cosineSimilarity(cluster.centroid, embedding);
|
|
336
|
+
if (!best || similarity > best.similarity) {
|
|
337
|
+
best = { clusterId: cluster.id, similarity };
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
return best;
|
|
341
|
+
}
|
|
342
|
+
async recomputeCentroid(clusterId) {
|
|
343
|
+
const rows = await this.db.query(
|
|
344
|
+
"SELECT n.embedding FROM nodes n JOIN cluster_memberships cm ON n.id = cm.node_id WHERE cm.cluster_id = $1",
|
|
345
|
+
[clusterId]
|
|
346
|
+
);
|
|
347
|
+
const vectors = rows.map((row) => row.embedding).filter(Boolean);
|
|
348
|
+
if (vectors.length === 0) return;
|
|
349
|
+
const centroid = averageVector(vectors);
|
|
350
|
+
await this.db.execute("UPDATE clusters SET centroid = $1, last_recomputed_at = NOW() WHERE id = $2", [
|
|
351
|
+
centroid,
|
|
352
|
+
clusterId
|
|
353
|
+
]);
|
|
354
|
+
}
|
|
355
|
+
async recomputeAllCentroids() {
|
|
356
|
+
const clusters = await this.db.query("SELECT id FROM clusters");
|
|
357
|
+
for (const cluster of clusters) {
|
|
358
|
+
await this.recomputeCentroid(cluster.id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async generateClusterLabel(clusterId) {
|
|
362
|
+
const rows = await this.db.query(
|
|
363
|
+
"SELECT n.label FROM nodes n JOIN cluster_memberships cm ON n.id = cm.node_id WHERE cm.cluster_id = $1 LIMIT 5",
|
|
364
|
+
[clusterId]
|
|
365
|
+
);
|
|
366
|
+
const label = rows.map((row) => row.label).join(", ");
|
|
367
|
+
await this.db.execute("UPDATE clusters SET label = $1 WHERE id = $2", [label, clusterId]);
|
|
368
|
+
return label;
|
|
369
|
+
}
|
|
370
|
+
async generateAllLabels() {
|
|
371
|
+
const clusters = await this.db.query("SELECT id FROM clusters");
|
|
372
|
+
for (const cluster of clusters) {
|
|
373
|
+
await this.generateClusterLabel(cluster.id);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
calculateSilhouetteScore(clusters) {
|
|
377
|
+
if (clusters.length === 0) return 0;
|
|
378
|
+
return clusters.reduce((acc, cluster) => acc + cluster.cohesion, 0) / clusters.length;
|
|
379
|
+
}
|
|
380
|
+
calculateCohesion(cluster) {
|
|
381
|
+
return cluster.cohesion;
|
|
382
|
+
}
|
|
383
|
+
async fetchNodesWithEmbeddings() {
|
|
384
|
+
const nodes = await this.db.query(
|
|
385
|
+
"SELECT * FROM nodes WHERE embedding IS NOT NULL"
|
|
386
|
+
);
|
|
387
|
+
return nodes;
|
|
388
|
+
}
|
|
389
|
+
runKmeans(nodes, config) {
|
|
390
|
+
const k = Math.max(1, config.clusterCount ?? 3);
|
|
391
|
+
const centroids = [];
|
|
392
|
+
const initialNodes = [...nodes].sort(() => Math.random() - 0.5).slice(0, k);
|
|
393
|
+
initialNodes.forEach((node) => centroids.push([...node.embedding]));
|
|
394
|
+
let assignments = /* @__PURE__ */ new Map();
|
|
395
|
+
for (let iteration = 0; iteration < 10; iteration += 1) {
|
|
396
|
+
assignments = /* @__PURE__ */ new Map();
|
|
397
|
+
nodes.forEach((node) => {
|
|
398
|
+
let bestIndex = 0;
|
|
399
|
+
let bestScore = -Infinity;
|
|
400
|
+
centroids.forEach((centroid, idx) => {
|
|
401
|
+
const score = cosineSimilarity(node.embedding, centroid);
|
|
402
|
+
if (score > bestScore) {
|
|
403
|
+
bestScore = score;
|
|
404
|
+
bestIndex = idx;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
assignments.set(node.id, bestIndex);
|
|
408
|
+
});
|
|
409
|
+
for (let i = 0; i < k; i += 1) {
|
|
410
|
+
const members = nodes.filter((node) => assignments.get(node.id) === i);
|
|
411
|
+
if (members.length === 0) continue;
|
|
412
|
+
centroids[i] = averageVector(members.map((node) => node.embedding));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const clusters = [];
|
|
416
|
+
for (let i = 0; i < k; i += 1) {
|
|
417
|
+
const members = nodes.filter((node) => assignments.get(node.id) === i);
|
|
418
|
+
if (members.length === 0) continue;
|
|
419
|
+
const centroid = centroids[i];
|
|
420
|
+
const similarities = members.map((node) => cosineSimilarity(node.embedding, centroid));
|
|
421
|
+
const avgSimilarity = similarities.reduce((acc, val) => acc + val, 0) / similarities.length;
|
|
422
|
+
clusters.push({
|
|
423
|
+
clusterId: crypto.randomUUID(),
|
|
424
|
+
label: members.map((node) => node.label).slice(0, 3).join(", "),
|
|
425
|
+
nodeIds: members.map((node) => node.id),
|
|
426
|
+
centroid,
|
|
427
|
+
avgSimilarity,
|
|
428
|
+
cohesion: avgSimilarity
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return clusters;
|
|
432
|
+
}
|
|
433
|
+
runDbscan(nodes, config) {
|
|
434
|
+
const epsilon = config.similarityThreshold ?? 0.75;
|
|
435
|
+
const minSamples = config.minSamples ?? 2;
|
|
436
|
+
const visited = /* @__PURE__ */ new Set();
|
|
437
|
+
const clusters = [];
|
|
438
|
+
for (const node of nodes) {
|
|
439
|
+
if (visited.has(node.id)) continue;
|
|
440
|
+
visited.add(node.id);
|
|
441
|
+
const neighbors = this.findNeighbors(nodes, node, epsilon);
|
|
442
|
+
if (neighbors.length < minSamples) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const clusterMembers = /* @__PURE__ */ new Set([node.id, ...neighbors.map((n) => n.id)]);
|
|
446
|
+
let idx = 0;
|
|
447
|
+
const neighborList = [...neighbors];
|
|
448
|
+
while (idx < neighborList.length) {
|
|
449
|
+
const neighbor = neighborList[idx];
|
|
450
|
+
if (!visited.has(neighbor.id)) {
|
|
451
|
+
visited.add(neighbor.id);
|
|
452
|
+
const neighborNeighbors = this.findNeighbors(nodes, neighbor, epsilon);
|
|
453
|
+
if (neighborNeighbors.length >= minSamples) {
|
|
454
|
+
neighborNeighbors.forEach((n) => {
|
|
455
|
+
if (!clusterMembers.has(n.id)) {
|
|
456
|
+
clusterMembers.add(n.id);
|
|
457
|
+
neighborList.push(n);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
idx += 1;
|
|
463
|
+
}
|
|
464
|
+
const members = nodes.filter((n) => clusterMembers.has(n.id));
|
|
465
|
+
const centroid = averageVector(members.map((member) => member.embedding));
|
|
466
|
+
const similarities = members.map((member) => cosineSimilarity(member.embedding, centroid));
|
|
467
|
+
const avgSimilarity = similarities.reduce((acc, val) => acc + val, 0) / similarities.length;
|
|
468
|
+
clusters.push({
|
|
469
|
+
clusterId: crypto.randomUUID(),
|
|
470
|
+
label: members.map((member) => member.label).slice(0, 3).join(", "),
|
|
471
|
+
nodeIds: members.map((member) => member.id),
|
|
472
|
+
centroid,
|
|
473
|
+
avgSimilarity,
|
|
474
|
+
cohesion: avgSimilarity
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return clusters;
|
|
478
|
+
}
|
|
479
|
+
findNeighbors(nodes, node, threshold) {
|
|
480
|
+
return nodes.filter((candidate) => {
|
|
481
|
+
if (candidate.id === node.id) return false;
|
|
482
|
+
return cosineSimilarity(candidate.embedding, node.embedding) >= threshold;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async persistClusters(clusters) {
|
|
486
|
+
await this.db.transaction(async (client) => {
|
|
487
|
+
await client.query("DELETE FROM cluster_memberships");
|
|
488
|
+
await client.query("DELETE FROM clusters");
|
|
489
|
+
for (const cluster of clusters) {
|
|
490
|
+
await client.query(
|
|
491
|
+
"INSERT INTO clusters (id, label, centroid, member_count, avg_similarity, cohesion, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())",
|
|
492
|
+
[
|
|
493
|
+
cluster.clusterId,
|
|
494
|
+
cluster.label,
|
|
495
|
+
cluster.centroid,
|
|
496
|
+
cluster.nodeIds.length,
|
|
497
|
+
cluster.avgSimilarity,
|
|
498
|
+
cluster.cohesion
|
|
499
|
+
]
|
|
500
|
+
);
|
|
501
|
+
for (const nodeId of cluster.nodeIds) {
|
|
502
|
+
await client.query(
|
|
503
|
+
"INSERT INTO cluster_memberships (node_id, cluster_id, similarity_score, is_primary) VALUES ($1, $2, $3, true)",
|
|
504
|
+
[nodeId, cluster.clusterId, cluster.avgSimilarity]
|
|
505
|
+
);
|
|
506
|
+
await client.query("UPDATE nodes SET cluster_id = $1, cluster_similarity = $2 WHERE id = $3", [
|
|
507
|
+
cluster.clusterId,
|
|
508
|
+
cluster.avgSimilarity,
|
|
509
|
+
nodeId
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
var INFERENCE_PROMPT = `You are analyzing potential relationships between concepts in a knowledge graph.
|
|
517
|
+
|
|
518
|
+
Node A:
|
|
519
|
+
- Label: {nodeA.label}
|
|
520
|
+
- Summary: {nodeA.summary}
|
|
521
|
+
- Content: {nodeA.content}
|
|
522
|
+
|
|
523
|
+
Node B:
|
|
524
|
+
- Label: {nodeB.label}
|
|
525
|
+
- Summary: {nodeB.summary}
|
|
526
|
+
- Content: {nodeB.content}
|
|
527
|
+
|
|
528
|
+
Determine if there is a meaningful relationship between these nodes.
|
|
529
|
+
|
|
530
|
+
Respond in JSON:
|
|
531
|
+
{
|
|
532
|
+
"hasRelationship": boolean,
|
|
533
|
+
"relationshipType": "related_to" | "derives_from" | "contradicts" | "supports" | "references" | "part_of" | "leads_to" | "similar_to",
|
|
534
|
+
"confidence": 0.0-1.0,
|
|
535
|
+
"reasoning": "Brief explanation",
|
|
536
|
+
"evidence": ["Specific quote or fact supporting this"]
|
|
537
|
+
}
|
|
538
|
+
`;
|
|
539
|
+
var RelationshipEngine = class {
|
|
540
|
+
constructor(db, config) {
|
|
541
|
+
this.db = db;
|
|
542
|
+
this.config = config;
|
|
543
|
+
this.client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY ?? "" });
|
|
544
|
+
}
|
|
545
|
+
client;
|
|
546
|
+
async inferForNode(nodeId) {
|
|
547
|
+
const candidates = await this.findCandidates(nodeId);
|
|
548
|
+
const inferred = [];
|
|
549
|
+
for (const candidate of candidates.slice(0, this.config.maxPerNode)) {
|
|
550
|
+
const inference = await this.inferPair(nodeId, candidate.nodeId);
|
|
551
|
+
if (inference && inference.confidence >= this.config.minConfidence) {
|
|
552
|
+
inferred.push(inference);
|
|
553
|
+
}
|
|
554
|
+
await this.applyRateLimit();
|
|
555
|
+
}
|
|
556
|
+
return inferred;
|
|
557
|
+
}
|
|
558
|
+
async inferForNodes(nodeIds) {
|
|
559
|
+
const inferred = [];
|
|
560
|
+
const errors = [];
|
|
561
|
+
for (const nodeId of nodeIds) {
|
|
562
|
+
try {
|
|
563
|
+
const results = await this.inferForNode(nodeId);
|
|
564
|
+
inferred.push(...results);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
errors.push({ nodeId, error: error instanceof Error ? error.message : String(error) });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return { inferred, errors };
|
|
570
|
+
}
|
|
571
|
+
async inferAll() {
|
|
572
|
+
const nodes = await this.db.query("SELECT id FROM nodes");
|
|
573
|
+
const result = await this.inferForNodes(nodes.map((node) => node.id));
|
|
574
|
+
return result.inferred;
|
|
575
|
+
}
|
|
576
|
+
async findCandidates(nodeId) {
|
|
577
|
+
const rows = await this.db.query(
|
|
578
|
+
`SELECT id, 1 - (embedding <=> (SELECT embedding FROM nodes WHERE id = $1)) as similarity
|
|
579
|
+
FROM nodes
|
|
580
|
+
WHERE embedding IS NOT NULL AND id != $1
|
|
581
|
+
ORDER BY embedding <=> (SELECT embedding FROM nodes WHERE id = $1)
|
|
582
|
+
LIMIT $2`,
|
|
583
|
+
[nodeId, this.config.maxPerNode * 3]
|
|
584
|
+
);
|
|
585
|
+
return rows.filter((row) => row.similarity >= this.config.similarityThreshold).map((row) => ({ nodeId: row.id, similarity: row.similarity }));
|
|
586
|
+
}
|
|
587
|
+
async validateRelationship(rel) {
|
|
588
|
+
return rel.confidence >= this.config.minConfidence;
|
|
589
|
+
}
|
|
590
|
+
async createEdgesFromInferences(inferences, autoApprove = true) {
|
|
591
|
+
const created = [];
|
|
592
|
+
if (!autoApprove) return created;
|
|
593
|
+
for (const inference of inferences) {
|
|
594
|
+
const rows = await this.db.query(
|
|
595
|
+
`INSERT INTO edges (from_node_id, to_node_id, relationship_type, strength, confidence, evidence, source, source_model)
|
|
596
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'ai_inferred', $7)
|
|
597
|
+
ON CONFLICT (from_node_id, to_node_id, relationship_type) DO NOTHING
|
|
598
|
+
RETURNING *`,
|
|
599
|
+
[
|
|
600
|
+
inference.fromNodeId,
|
|
601
|
+
inference.toNodeId,
|
|
602
|
+
inference.relationshipType,
|
|
603
|
+
inference.confidence,
|
|
604
|
+
inference.confidence,
|
|
605
|
+
JSON.stringify(inference.evidence),
|
|
606
|
+
this.config.model
|
|
607
|
+
]
|
|
608
|
+
);
|
|
609
|
+
if (rows[0]) {
|
|
610
|
+
created.push(rows[0]);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return created;
|
|
614
|
+
}
|
|
615
|
+
async inferPair(fromNodeId, toNodeId) {
|
|
616
|
+
const [nodeA, nodeB] = await Promise.all([
|
|
617
|
+
this.db.queryOne(
|
|
618
|
+
"SELECT id, label, summary, content FROM nodes WHERE id = $1",
|
|
619
|
+
[fromNodeId]
|
|
620
|
+
),
|
|
621
|
+
this.db.queryOne(
|
|
622
|
+
"SELECT id, label, summary, content FROM nodes WHERE id = $1",
|
|
623
|
+
[toNodeId]
|
|
624
|
+
)
|
|
625
|
+
]);
|
|
626
|
+
if (!nodeA || !nodeB) return null;
|
|
627
|
+
const prompt = INFERENCE_PROMPT.replace("{nodeA.label}", nodeA.label).replace("{nodeA.summary}", nodeA.summary ?? "").replace("{nodeA.content}", nodeA.content ?? "").replace("{nodeB.label}", nodeB.label).replace("{nodeB.summary}", nodeB.summary ?? "").replace("{nodeB.content}", nodeB.content ?? "");
|
|
628
|
+
const response = await this.client.chat.completions.create({
|
|
629
|
+
model: this.config.model,
|
|
630
|
+
messages: [{ role: "user", content: prompt }],
|
|
631
|
+
response_format: { type: "json_object" }
|
|
632
|
+
});
|
|
633
|
+
const content = response.choices[0]?.message?.content;
|
|
634
|
+
if (!content) return null;
|
|
635
|
+
const parsed = JSON.parse(content);
|
|
636
|
+
if (!parsed.hasRelationship) return null;
|
|
637
|
+
return {
|
|
638
|
+
fromNodeId,
|
|
639
|
+
toNodeId,
|
|
640
|
+
relationshipType: parsed.relationshipType ?? "related_to",
|
|
641
|
+
confidence: parsed.confidence ?? 0,
|
|
642
|
+
reasoning: parsed.reasoning ?? "",
|
|
643
|
+
evidence: (parsed.evidence ?? []).map((text) => ({ type: "text", content: text }))
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async applyRateLimit() {
|
|
647
|
+
const delayMs = Math.ceil(6e4 / this.config.rateLimit);
|
|
648
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// src/core/analysis/pipeline.ts
|
|
653
|
+
var AnalysisPipeline = class {
|
|
654
|
+
constructor(db, embeddings, clustering, relationships, events) {
|
|
655
|
+
this.db = db;
|
|
656
|
+
this.embeddings = embeddings;
|
|
657
|
+
this.clustering = clustering;
|
|
658
|
+
this.relationships = relationships;
|
|
659
|
+
this.events = events;
|
|
660
|
+
}
|
|
661
|
+
activeJobs = /* @__PURE__ */ new Map();
|
|
662
|
+
async runFull(options = {}) {
|
|
663
|
+
const job = await this.createJob("full_analysis", options);
|
|
664
|
+
try {
|
|
665
|
+
if (!options.skipEmbeddings) {
|
|
666
|
+
await this.runEmbeddingsStage(job, options);
|
|
667
|
+
}
|
|
668
|
+
if (!options.skipClustering) {
|
|
669
|
+
await this.runClusteringStage(job, options);
|
|
670
|
+
}
|
|
671
|
+
if (!options.skipRelationships) {
|
|
672
|
+
await this.runRelationshipsStage(job, options);
|
|
673
|
+
}
|
|
674
|
+
return await this.completeJob(job.id);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
return await this.failJob(job.id, error);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async runEmbeddings(options = {}) {
|
|
680
|
+
const job = await this.createJob("embedding", options);
|
|
681
|
+
try {
|
|
682
|
+
await this.runEmbeddingsStage(job, options);
|
|
683
|
+
return await this.completeJob(job.id);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
return await this.failJob(job.id, error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async runClustering(options = {}) {
|
|
689
|
+
const job = await this.createJob("clustering", options);
|
|
690
|
+
try {
|
|
691
|
+
await this.runClusteringStage(job, options);
|
|
692
|
+
return await this.completeJob(job.id);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
return await this.failJob(job.id, error);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async runRelationships(options = {}) {
|
|
698
|
+
const job = await this.createJob("relationship_inference", options);
|
|
699
|
+
try {
|
|
700
|
+
await this.runRelationshipsStage(job, options);
|
|
701
|
+
return await this.completeJob(job.id);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
return await this.failJob(job.id, error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async getJob(jobId) {
|
|
707
|
+
const row = await this.db.queryOne("SELECT * FROM analysis_runs WHERE id = $1", [jobId]);
|
|
708
|
+
return row ?? null;
|
|
709
|
+
}
|
|
710
|
+
async listJobs(options) {
|
|
711
|
+
const where = options?.status ? "WHERE status = $1" : "";
|
|
712
|
+
const limit = options?.limit ? `LIMIT ${options.limit}` : "";
|
|
713
|
+
const rows = await this.db.query(
|
|
714
|
+
`SELECT * FROM analysis_runs ${where} ORDER BY created_at DESC ${limit}`,
|
|
715
|
+
options?.status ? [options.status] : []
|
|
716
|
+
);
|
|
717
|
+
return rows;
|
|
718
|
+
}
|
|
719
|
+
async cancelJob(jobId) {
|
|
720
|
+
const controller = this.activeJobs.get(jobId);
|
|
721
|
+
if (!controller) return false;
|
|
722
|
+
controller.abort();
|
|
723
|
+
await this.db.execute("UPDATE analysis_runs SET status = $1 WHERE id = $2", [
|
|
724
|
+
"cancelled",
|
|
725
|
+
jobId
|
|
726
|
+
]);
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
getActiveJobs() {
|
|
730
|
+
return [];
|
|
731
|
+
}
|
|
732
|
+
isRunning() {
|
|
733
|
+
return this.activeJobs.size > 0;
|
|
734
|
+
}
|
|
735
|
+
async runEmbeddingsStage(job, options) {
|
|
736
|
+
const nodeIds = await this.resolveNodeIds(options.nodeIds, options.forceRecompute);
|
|
737
|
+
const { results, errors } = await this.embeddings.embedNodes(nodeIds);
|
|
738
|
+
await this.updateJobResults(job.id, {
|
|
739
|
+
embeddingsGenerated: results.length,
|
|
740
|
+
errors
|
|
741
|
+
});
|
|
742
|
+
options.onProgress?.({
|
|
743
|
+
stage: "embeddings",
|
|
744
|
+
progress: 100,
|
|
745
|
+
currentItem: "complete",
|
|
746
|
+
itemsProcessed: results.length,
|
|
747
|
+
totalItems: nodeIds.length
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async runClusteringStage(job, options) {
|
|
751
|
+
const clusterCount = options.clusterCount;
|
|
752
|
+
const algorithm = options.clusteringAlgorithm ?? "kmeans";
|
|
753
|
+
const result = await this.clustering.clusterNodes({
|
|
754
|
+
algorithm,
|
|
755
|
+
clusterCount,
|
|
756
|
+
similarityThreshold: options.relationshipThreshold
|
|
757
|
+
});
|
|
758
|
+
await this.updateJobResults(job.id, {
|
|
759
|
+
clustersCreated: result.clusters.length
|
|
760
|
+
});
|
|
761
|
+
options.onProgress?.({
|
|
762
|
+
stage: "clustering",
|
|
763
|
+
progress: 100,
|
|
764
|
+
currentItem: "complete",
|
|
765
|
+
itemsProcessed: result.clusters.length,
|
|
766
|
+
totalItems: result.clusters.length
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
async runRelationshipsStage(job, options) {
|
|
770
|
+
const nodeIds = options.nodeIds ?? await this.resolveNodeIds();
|
|
771
|
+
const { inferred, errors } = await this.relationships.inferForNodes(nodeIds);
|
|
772
|
+
await this.relationships.createEdgesFromInferences(inferred, true);
|
|
773
|
+
await this.updateJobResults(job.id, {
|
|
774
|
+
relationshipsInferred: inferred.length,
|
|
775
|
+
errors
|
|
776
|
+
});
|
|
777
|
+
options.onProgress?.({
|
|
778
|
+
stage: "relationships",
|
|
779
|
+
progress: 100,
|
|
780
|
+
currentItem: "complete",
|
|
781
|
+
itemsProcessed: inferred.length,
|
|
782
|
+
totalItems: inferred.length
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async createJob(runType, options) {
|
|
786
|
+
const controller = new AbortController();
|
|
787
|
+
const row = await this.db.queryOne(
|
|
788
|
+
`INSERT INTO analysis_runs (run_type, input_params, status, progress, started_at)
|
|
789
|
+
VALUES ($1, $2, 'running', 0, NOW())
|
|
790
|
+
RETURNING *`,
|
|
791
|
+
[runType, options]
|
|
792
|
+
);
|
|
793
|
+
if (!row) throw new Error("Failed to create analysis job");
|
|
794
|
+
this.activeJobs.set(row.id, controller);
|
|
795
|
+
return row;
|
|
796
|
+
}
|
|
797
|
+
async completeJob(jobId) {
|
|
798
|
+
const row = await this.db.queryOne(
|
|
799
|
+
`UPDATE analysis_runs
|
|
800
|
+
SET status = 'completed', progress = 100, completed_at = NOW(), duration_ms = EXTRACT(EPOCH FROM (NOW() - started_at)) * 1000
|
|
801
|
+
WHERE id = $1
|
|
802
|
+
RETURNING *`,
|
|
803
|
+
[jobId]
|
|
804
|
+
);
|
|
805
|
+
this.activeJobs.delete(jobId);
|
|
806
|
+
return row;
|
|
807
|
+
}
|
|
808
|
+
async failJob(jobId, error) {
|
|
809
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
810
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
811
|
+
const row = await this.db.queryOne(
|
|
812
|
+
`UPDATE analysis_runs
|
|
813
|
+
SET status = 'failed', error_message = $2, error_stack = $3, completed_at = NOW(), duration_ms = EXTRACT(EPOCH FROM (NOW() - started_at)) * 1000
|
|
814
|
+
WHERE id = $1
|
|
815
|
+
RETURNING *`,
|
|
816
|
+
[jobId, message, stack]
|
|
817
|
+
);
|
|
818
|
+
this.activeJobs.delete(jobId);
|
|
819
|
+
return row;
|
|
820
|
+
}
|
|
821
|
+
async updateJobResults(jobId, updates) {
|
|
822
|
+
const row = await this.db.queryOne(
|
|
823
|
+
"SELECT results FROM analysis_runs WHERE id = $1",
|
|
824
|
+
[jobId]
|
|
825
|
+
);
|
|
826
|
+
const current = row?.results ?? {
|
|
827
|
+
nodesProcessed: 0,
|
|
828
|
+
embeddingsGenerated: 0,
|
|
829
|
+
clustersCreated: 0,
|
|
830
|
+
relationshipsInferred: 0,
|
|
831
|
+
errors: []
|
|
832
|
+
};
|
|
833
|
+
const next = { ...current, ...updates };
|
|
834
|
+
await this.db.execute("UPDATE analysis_runs SET results = $1 WHERE id = $2", [next, jobId]);
|
|
835
|
+
}
|
|
836
|
+
async resolveNodeIds(nodeIds, forceRecompute = false) {
|
|
837
|
+
if (nodeIds?.length) return nodeIds;
|
|
838
|
+
if (forceRecompute) {
|
|
839
|
+
const rows2 = await this.db.query("SELECT id FROM nodes");
|
|
840
|
+
return rows2.map((row) => row.id);
|
|
841
|
+
}
|
|
842
|
+
const rows = await this.db.query("SELECT id FROM nodes WHERE embedding IS NULL");
|
|
843
|
+
return rows.map((row) => row.id);
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
var Database = class {
|
|
847
|
+
pool;
|
|
848
|
+
slowQueryThresholdMs;
|
|
849
|
+
constructor(config) {
|
|
850
|
+
const poolConfig = {
|
|
851
|
+
connectionString: config.connectionString ?? this.buildConnectionString(config),
|
|
852
|
+
ssl: config.ssl,
|
|
853
|
+
min: config.pool?.min,
|
|
854
|
+
max: config.pool?.max,
|
|
855
|
+
idleTimeoutMillis: config.pool?.idleTimeoutMs,
|
|
856
|
+
connectionTimeoutMillis: config.pool?.connectionTimeoutMs
|
|
857
|
+
};
|
|
858
|
+
this.pool = new Pool(poolConfig);
|
|
859
|
+
this.slowQueryThresholdMs = config.slowQueryThresholdMs;
|
|
860
|
+
}
|
|
861
|
+
async connect() {
|
|
862
|
+
const client = await this.pool.connect();
|
|
863
|
+
client.release();
|
|
864
|
+
}
|
|
865
|
+
async disconnect() {
|
|
866
|
+
await this.pool.end();
|
|
867
|
+
}
|
|
868
|
+
async isConnected() {
|
|
869
|
+
try {
|
|
870
|
+
await this.query("SELECT 1");
|
|
871
|
+
return true;
|
|
872
|
+
} catch {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async query(sql, params = []) {
|
|
877
|
+
const start = Date.now();
|
|
878
|
+
const result = await this.pool.query(sql, params);
|
|
879
|
+
this.logSlowQuery(sql, Date.now() - start);
|
|
880
|
+
return result.rows;
|
|
881
|
+
}
|
|
882
|
+
async queryOne(sql, params = []) {
|
|
883
|
+
const rows = await this.query(sql, params);
|
|
884
|
+
return rows[0] ?? null;
|
|
885
|
+
}
|
|
886
|
+
async execute(sql, params = []) {
|
|
887
|
+
const start = Date.now();
|
|
888
|
+
const result = await this.pool.query(sql, params);
|
|
889
|
+
this.logSlowQuery(sql, Date.now() - start);
|
|
890
|
+
return result.rowCount ?? 0;
|
|
891
|
+
}
|
|
892
|
+
async transaction(fn) {
|
|
893
|
+
const client = await this.pool.connect();
|
|
894
|
+
try {
|
|
895
|
+
await client.query("BEGIN");
|
|
896
|
+
const result = await fn(client);
|
|
897
|
+
await client.query("COMMIT");
|
|
898
|
+
return result;
|
|
899
|
+
} catch (error) {
|
|
900
|
+
await client.query("ROLLBACK");
|
|
901
|
+
throw error;
|
|
902
|
+
} finally {
|
|
903
|
+
client.release();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async tableExists(tableName) {
|
|
907
|
+
const result = await this.queryOne(
|
|
908
|
+
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1) as exists",
|
|
909
|
+
[tableName]
|
|
910
|
+
);
|
|
911
|
+
return result?.exists ?? false;
|
|
912
|
+
}
|
|
913
|
+
async getPoolStats() {
|
|
914
|
+
return {
|
|
915
|
+
totalCount: this.pool.totalCount,
|
|
916
|
+
idleCount: this.pool.idleCount,
|
|
917
|
+
waitingCount: this.pool.waitingCount
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
buildConnectionString(config) {
|
|
921
|
+
if (config.host && config.port && config.user && config.database) {
|
|
922
|
+
const password = config.password ? `:${config.password}` : "";
|
|
923
|
+
return `postgresql://${config.user}${password}@${config.host}:${config.port}/${config.database}`;
|
|
924
|
+
}
|
|
925
|
+
return void 0;
|
|
926
|
+
}
|
|
927
|
+
logSlowQuery(sql, durationMs) {
|
|
928
|
+
if (!this.slowQueryThresholdMs || durationMs < this.slowQueryThresholdMs) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
console.warn(`[omi-neuron] Slow query (${durationMs}ms): ${sql}`);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
export { AnalysisPipeline, ClusteringEngine, Database, EmbeddingsService, EventBus, RelationshipEngine, createEvent };
|
|
936
|
+
//# sourceMappingURL=chunk-RQCGONPN.js.map
|
|
937
|
+
//# sourceMappingURL=chunk-RQCGONPN.js.map
|