@promptev/context-engine 0.0.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.
Files changed (68) hide show
  1. package/LICENSE.md +202 -0
  2. package/NOTICE +17 -0
  3. package/README.md +112 -0
  4. package/dist/cli.js +11998 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/config-Bl9U789m.d.cts +174 -0
  7. package/dist/config-Bt9bUQqU.d.ts +174 -0
  8. package/dist/embeddings-B-jZ42mk.d.cts +67 -0
  9. package/dist/embeddings-DaSdAZN3.d.ts +67 -0
  10. package/dist/express.cjs +3173 -0
  11. package/dist/express.cjs.map +1 -0
  12. package/dist/express.d.cts +24 -0
  13. package/dist/express.d.ts +24 -0
  14. package/dist/express.js +3170 -0
  15. package/dist/express.js.map +1 -0
  16. package/dist/fastify.cjs +3184 -0
  17. package/dist/fastify.cjs.map +1 -0
  18. package/dist/fastify.d.cts +16 -0
  19. package/dist/fastify.d.ts +16 -0
  20. package/dist/fastify.js +3181 -0
  21. package/dist/fastify.js.map +1 -0
  22. package/dist/governance-BDkcv4qZ.d.cts +79 -0
  23. package/dist/governance-XIScatRO.d.ts +79 -0
  24. package/dist/graph/index.cjs +1428 -0
  25. package/dist/graph/index.cjs.map +1 -0
  26. package/dist/graph/index.d.cts +104 -0
  27. package/dist/graph/index.d.ts +104 -0
  28. package/dist/graph/index.js +1413 -0
  29. package/dist/graph/index.js.map +1 -0
  30. package/dist/hono.cjs +3183 -0
  31. package/dist/hono.cjs.map +1 -0
  32. package/dist/hono.d.cts +39 -0
  33. package/dist/hono.d.ts +39 -0
  34. package/dist/hono.js +3179 -0
  35. package/dist/hono.js.map +1 -0
  36. package/dist/index.cjs +11731 -0
  37. package/dist/index.cjs.map +1 -0
  38. package/dist/index.d.cts +851 -0
  39. package/dist/index.d.ts +851 -0
  40. package/dist/index.js +11676 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/mcp.cjs +181 -0
  43. package/dist/mcp.cjs.map +1 -0
  44. package/dist/mcp.d.cts +26 -0
  45. package/dist/mcp.d.ts +26 -0
  46. package/dist/mcp.js +179 -0
  47. package/dist/mcp.js.map +1 -0
  48. package/dist/migrations/sql/0001.sql +119 -0
  49. package/dist/migrations/sql/0002_graph.sql +48 -0
  50. package/dist/migrations/sql/0003_tools.sql +61 -0
  51. package/dist/migrations/sql/0004_acl_indexes.sql +4 -0
  52. package/dist/redaction-BmDSWJ7h.d.cts +98 -0
  53. package/dist/redaction-BmDSWJ7h.d.ts +98 -0
  54. package/dist/redaction-presidio.cjs +79 -0
  55. package/dist/redaction-presidio.cjs.map +1 -0
  56. package/dist/redaction-presidio.d.cts +22 -0
  57. package/dist/redaction-presidio.d.ts +22 -0
  58. package/dist/redaction-presidio.js +73 -0
  59. package/dist/redaction-presidio.js.map +1 -0
  60. package/dist/router-CrxZ2y_Z.d.ts +82 -0
  61. package/dist/router-OPgSoYAB.d.cts +82 -0
  62. package/dist/skills/context-engine/SKILL.md +160 -0
  63. package/package.json +184 -0
  64. package/src/migrations/sql/0001.sql +119 -0
  65. package/src/migrations/sql/0002_graph.sql +48 -0
  66. package/src/migrations/sql/0003_tools.sql +61 -0
  67. package/src/migrations/sql/0004_acl_indexes.sql +4 -0
  68. package/src/skills/context-engine/SKILL.md +160 -0
@@ -0,0 +1,1428 @@
1
+ 'use strict';
2
+
3
+ var OpenAI = require('openai');
4
+ var crypto = require('crypto');
5
+ var jsonrepair = require('jsonrepair');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
10
+
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __esm = (fn, res) => function __init() {
14
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
+ };
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+
21
+ // src/errors.ts
22
+ var ExtraMissingError;
23
+ var init_errors = __esm({
24
+ "src/errors.ts"() {
25
+ ExtraMissingError = class extends Error {
26
+ constructor(extra, pkg, what) {
27
+ super(`${what} requires the '${pkg}' package (optional extra: ${extra}): npm install ${pkg}`);
28
+ this.name = "ExtraMissingError";
29
+ }
30
+ };
31
+ }
32
+ });
33
+
34
+ // src/extras.ts
35
+ async function requireExtra(specifier, extra, what) {
36
+ try {
37
+ return await import(specifier);
38
+ } catch (_err) {
39
+ throw new ExtraMissingError(extra, specifier, what);
40
+ }
41
+ }
42
+ var init_extras = __esm({
43
+ "src/extras.ts"() {
44
+ init_errors();
45
+ }
46
+ });
47
+
48
+ // src/hooks.ts
49
+ function emitError(hooks, exc, ctx) {
50
+ log.error("context_engine error:", exc, "| ctx=", ctx);
51
+ if (!hooks?.onError) return;
52
+ try {
53
+ hooks.onError(exc, ctx);
54
+ } catch {
55
+ log.warn("onError callback raised; swallowing");
56
+ }
57
+ }
58
+ var log;
59
+ var init_hooks = __esm({
60
+ "src/hooks.ts"() {
61
+ log = {
62
+ warn: (...args) => console.warn("[context-engine]", ...args),
63
+ error: (...args) => console.error("[context-engine]", ...args)
64
+ };
65
+ }
66
+ });
67
+
68
+ // src/providers/llm.ts
69
+ var llm_exports = {};
70
+ __export(llm_exports, {
71
+ LLMClient: () => LLMClient,
72
+ TIMEOUT_MS: () => TIMEOUT_MS,
73
+ buildLlmClient: () => buildLlmClient,
74
+ buildOpenAIChatClient: () => buildOpenAIChatClient,
75
+ callLlm: () => callLlm
76
+ });
77
+ function toBase64(bytes) {
78
+ return Buffer.from(bytes).toString("base64");
79
+ }
80
+ async function postJson(fetchImpl, url, body, headers) {
81
+ const resp = await fetchImpl(url, {
82
+ method: "POST",
83
+ headers: { "content-type": "application/json", ...headers },
84
+ body: JSON.stringify(body),
85
+ signal: AbortSignal.timeout(TIMEOUT_MS)
86
+ });
87
+ if (!resp.ok) {
88
+ const text = await resp.text().catch(() => "");
89
+ throw new Error(`HTTP ${resp.status} ${resp.statusText}${text ? `: ${text}` : ""}`);
90
+ }
91
+ return resp.json();
92
+ }
93
+ function buildOpenAIChatClient(cfg) {
94
+ return new OpenAI__default.default({
95
+ apiKey: cfg.apiKey ?? void 0,
96
+ baseURL: cfg.baseUrl ?? void 0,
97
+ timeout: TIMEOUT_MS,
98
+ maxRetries: 0
99
+ });
100
+ }
101
+ async function loadGeminiChatClient(apiKey) {
102
+ const specifier = "@google/genai";
103
+ let mod;
104
+ try {
105
+ mod = await import(specifier);
106
+ } catch {
107
+ throw new ExtraMissingError("gemini", specifier, "gemini llm");
108
+ }
109
+ const Ctor = mod.GoogleGenAI ?? mod.Client;
110
+ if (!Ctor) {
111
+ throw new ExtraMissingError("gemini", specifier, "gemini llm");
112
+ }
113
+ return new Ctor({ apiKey: apiKey ?? null });
114
+ }
115
+ async function loadBedrockSdk() {
116
+ const specifier = "@aws-sdk/client-bedrock-runtime";
117
+ try {
118
+ return await import(specifier);
119
+ } catch {
120
+ throw new ExtraMissingError("bedrock", specifier, "bedrock llm");
121
+ }
122
+ }
123
+ function buildLlmClient(cfg, opts) {
124
+ if (opts?.client || opts?.fetch || opts?.fetchImpl) {
125
+ return new LLMClient(cfg, opts);
126
+ }
127
+ if (cfg.provider === "anthropic") {
128
+ return new LLMClient(cfg, { fetch: globalThis.fetch });
129
+ }
130
+ if (OPENAI_FAMILY.has(cfg.provider)) {
131
+ return new LLMClient(cfg, { client: buildOpenAIChatClient(cfg) });
132
+ }
133
+ if (cfg.provider === "gemini" || cfg.provider === "bedrock") {
134
+ return new LLMClient(cfg);
135
+ }
136
+ throw new Error(`unknown llm provider: ${JSON.stringify(cfg.provider)}`);
137
+ }
138
+ async function callLlm(cfg, opts) {
139
+ const { client, ...callOpts } = opts;
140
+ if (client) {
141
+ return client.call(callOpts);
142
+ }
143
+ const owned = buildLlmClient(cfg);
144
+ try {
145
+ return await owned.call(callOpts);
146
+ } finally {
147
+ await owned.aclose();
148
+ }
149
+ }
150
+ var TIMEOUT_MS, ANTHROPIC_VERSION, ANTHROPIC_MAX_TOKENS, OPENAI_FAMILY, LLMClient;
151
+ var init_llm = __esm({
152
+ "src/providers/llm.ts"() {
153
+ init_errors();
154
+ TIMEOUT_MS = 3e4;
155
+ ANTHROPIC_VERSION = "2023-06-01";
156
+ ANTHROPIC_MAX_TOKENS = 4096;
157
+ OPENAI_FAMILY = /* @__PURE__ */ new Set(["openai", "azure_openai", "custom"]);
158
+ LLMClient = class {
159
+ cfg;
160
+ provider;
161
+ model;
162
+ client;
163
+ fetchImpl;
164
+ genaiClient = null;
165
+ constructor(cfg, opts = {}) {
166
+ this.cfg = cfg;
167
+ this.provider = cfg.provider;
168
+ this.model = cfg.model;
169
+ this.client = opts.client ?? null;
170
+ this.fetchImpl = opts.fetch ?? opts.fetchImpl ?? null;
171
+ }
172
+ async aclose() {
173
+ const closer = this.client?.close;
174
+ if (typeof closer === "function") {
175
+ await closer.call(this.client);
176
+ }
177
+ }
178
+ async [Symbol.asyncDispose]() {
179
+ await this.aclose();
180
+ }
181
+ async call(opts) {
182
+ const { system, user, jsonMode = false, images = null } = opts;
183
+ if (this.provider === "anthropic") {
184
+ return this.callAnthropic(system, user, jsonMode, images);
185
+ }
186
+ if (OPENAI_FAMILY.has(this.provider)) {
187
+ return this.callOpenAI(system, user, jsonMode, images);
188
+ }
189
+ if (this.provider === "gemini") {
190
+ return this.callGemini(system, user, jsonMode, images);
191
+ }
192
+ if (this.provider === "bedrock") {
193
+ return this.callBedrock(system, user, jsonMode, images);
194
+ }
195
+ throw new Error(`unknown llm provider: ${JSON.stringify(this.provider)}`);
196
+ }
197
+ async callAnthropic(system, user, jsonMode, images) {
198
+ if (!this.fetchImpl) {
199
+ throw new Error("anthropic llm client has no fetch implementation");
200
+ }
201
+ if (jsonMode) {
202
+ system = `${system}
203
+
204
+ Respond with valid JSON only.`;
205
+ }
206
+ const content = [];
207
+ for (const image of images ?? []) {
208
+ content.push({
209
+ type: "image",
210
+ source: {
211
+ type: "base64",
212
+ media_type: "image/png",
213
+ data: toBase64(image)
214
+ }
215
+ });
216
+ }
217
+ content.push({ type: "text", text: user });
218
+ const data = await postJson(
219
+ this.fetchImpl,
220
+ "https://api.anthropic.com/v1/messages",
221
+ {
222
+ model: this.model,
223
+ max_tokens: ANTHROPIC_MAX_TOKENS,
224
+ system,
225
+ messages: [{ role: "user", content }]
226
+ },
227
+ {
228
+ "x-api-key": this.cfg.apiKey ?? "",
229
+ "anthropic-version": ANTHROPIC_VERSION,
230
+ "content-type": "application/json"
231
+ }
232
+ );
233
+ const text = data.content[0].text;
234
+ const usage = data.usage ?? {};
235
+ return [text, { input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 }];
236
+ }
237
+ async callOpenAI(system, user, jsonMode, images) {
238
+ if (!this.client) {
239
+ throw new Error("openai-family llm client has no client");
240
+ }
241
+ const content = [{ type: "text", text: user }];
242
+ for (const image of images ?? []) {
243
+ content.push({
244
+ type: "image_url",
245
+ image_url: { url: `data:image/png;base64,${toBase64(image)}` }
246
+ });
247
+ }
248
+ const body = {
249
+ model: this.model,
250
+ messages: [
251
+ { role: "system", content: system },
252
+ { role: "user", content }
253
+ ]
254
+ };
255
+ if (jsonMode) {
256
+ body.response_format = { type: "json_object" };
257
+ }
258
+ const resp = await this.client.chat.completions.create(body);
259
+ const text = resp.choices[0]?.message?.content ?? "";
260
+ const usage = resp.usage;
261
+ return [text, { input: usage?.prompt_tokens ?? 0, output: usage?.completion_tokens ?? 0 }];
262
+ }
263
+ async callGemini(system, user, jsonMode, images) {
264
+ if (!this.genaiClient) {
265
+ this.genaiClient = await loadGeminiChatClient(this.cfg.apiKey);
266
+ }
267
+ const parts = [];
268
+ for (const img of images ?? []) {
269
+ parts.push({ inlineData: { mimeType: "image/png", data: toBase64(img) } });
270
+ }
271
+ parts.push({ text: user });
272
+ const config = { systemInstruction: system };
273
+ if (jsonMode) {
274
+ config.responseMimeType = "application/json";
275
+ }
276
+ const resp = await this.genaiClient.models.generateContent({
277
+ model: this.model,
278
+ contents: parts,
279
+ config
280
+ });
281
+ const text = resp.text ?? "";
282
+ const usageMeta = resp.usageMetadata;
283
+ const inputTokens = usageMeta?.promptTokenCount ?? usageMeta?.prompt_token_count ?? 0;
284
+ const outputTokens = usageMeta?.candidatesTokenCount ?? usageMeta?.candidates_token_count ?? 0;
285
+ return [text, { input: inputTokens || 0, output: outputTokens || 0 }];
286
+ }
287
+ async callBedrock(system, user, jsonMode, images) {
288
+ const { BedrockRuntimeClient, ConverseCommand } = await loadBedrockSdk();
289
+ if (jsonMode) {
290
+ system = `${system}
291
+
292
+ Respond with valid JSON only.`;
293
+ }
294
+ const content = [];
295
+ for (const image of images ?? []) {
296
+ content.push({ image: { format: "png", source: { bytes: image } } });
297
+ }
298
+ content.push({ text: user });
299
+ const runtime = new BedrockRuntimeClient({
300
+ maxAttempts: 1,
301
+ requestHandler: {
302
+ requestTimeout: TIMEOUT_MS,
303
+ connectionTimeout: TIMEOUT_MS
304
+ }
305
+ });
306
+ try {
307
+ const result = await runtime.send(
308
+ new ConverseCommand({
309
+ modelId: this.model,
310
+ system: [{ text: system }],
311
+ messages: [{ role: "user", content }]
312
+ })
313
+ );
314
+ const text = result.output?.message?.content?.[0]?.text ?? "";
315
+ const usage = result.usage ?? {};
316
+ return [text, { input: usage.inputTokens ?? 0, output: usage.outputTokens ?? 0 }];
317
+ } finally {
318
+ runtime.destroy?.();
319
+ }
320
+ }
321
+ };
322
+ }
323
+ });
324
+ function mulberry32(seed) {
325
+ let a = seed >>> 0;
326
+ return () => {
327
+ a |= 0;
328
+ a = a + 1831565813 | 0;
329
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
330
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
331
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
332
+ };
333
+ }
334
+ function partitionGraph(G, louvain, opts = {}) {
335
+ const maxLevels = opts.maxLevels ?? exports.MAX_HIERARCHY_LEVELS;
336
+ const minSize = opts.minSize ?? MIN_COMMUNITY_SIZE;
337
+ const seed = opts.seed ?? LOUVAIN_SEED;
338
+ const communities = [];
339
+ const seen = /* @__PURE__ */ new Set();
340
+ for (let level = 0; level < maxLevels; level++) {
341
+ const resolution = 1 / (level + 1);
342
+ const assignment = louvain(G, { resolution, rng: mulberry32(seed + level) });
343
+ const buckets = /* @__PURE__ */ new Map();
344
+ for (const [node, cid] of Object.entries(assignment)) {
345
+ const list = buckets.get(cid) ?? [];
346
+ list.push(node);
347
+ buckets.set(cid, list);
348
+ }
349
+ const kept = [...buckets.values()].filter((m) => m.length >= minSize).map((m) => [...m].sort());
350
+ const key = JSON.stringify([...kept].map((m) => [...m].sort()).sort());
351
+ if (seen.has(key)) continue;
352
+ seen.add(key);
353
+ for (const members of kept) communities.push({ level, members });
354
+ }
355
+ return communities;
356
+ }
357
+ async function loadLouvain() {
358
+ const mod = await requireExtra(
359
+ "graphology-communities-louvain",
360
+ "graph",
361
+ "community detection"
362
+ );
363
+ const fn = mod.default ?? mod;
364
+ return fn;
365
+ }
366
+ async function detectWithPython(pool) {
367
+ const entities = (await pool.query(`SELECT id FROM context_engine_entities`)).rows;
368
+ if (entities.length < MIN_COMMUNITY_SIZE) return [];
369
+ const entityMap = /* @__PURE__ */ new Map();
370
+ const entityIds = [];
371
+ entities.forEach((e, i) => {
372
+ entityMap.set(String(e.id), i);
373
+ entityIds.push(String(e.id));
374
+ });
375
+ const relationships = (await pool.query(`SELECT source_entity_id, target_entity_id FROM context_engine_entity_relationships`)).rows;
376
+ const graphology = await requireExtra(
377
+ "graphology",
378
+ "graph",
379
+ "community detection"
380
+ );
381
+ const Graph = graphology.default;
382
+ if (!Graph) throw new Error("graphology export shape is unexpected");
383
+ const louvain = await loadLouvain();
384
+ const G = new Graph({ type: "undirected" });
385
+ for (let i = 0; i < entities.length; i++) G.addNode(String(i));
386
+ let hasEdge = false;
387
+ for (const rel of relationships) {
388
+ const src = entityMap.get(String(rel.source_entity_id));
389
+ const tgt = entityMap.get(String(rel.target_entity_id));
390
+ if (src != null && tgt != null && src !== tgt) {
391
+ hasEdge = true;
392
+ const a = String(src);
393
+ const b = String(tgt);
394
+ if (G.hasEdge(a, b)) {
395
+ const w = Number(G.getEdgeAttribute(a, b, "weight") ?? 1);
396
+ G.setEdgeAttribute(a, b, "weight", w + 1);
397
+ } else {
398
+ G.addEdge(a, b, { weight: 1 });
399
+ }
400
+ }
401
+ }
402
+ if (!hasEdge) return [];
403
+ const partitions = partitionGraph(G, louvain);
404
+ return partitions.map((p) => ({
405
+ level: p.level,
406
+ entity_ids: p.members.map((idx) => entityIds[Number(idx)])
407
+ }));
408
+ }
409
+ async function summarizeCommunities(pending, config) {
410
+ if (!pending.length || !config.graph.extractionLlm) return pending.map(() => null);
411
+ const blocks = pending.map((pc, idx) => {
412
+ const entityList = (pc.entities ?? []).slice(0, MAX_SUMMARY_ENTITIES);
413
+ const entityLines = entityList.map((e) => ` - ${e.name} (${e.type}, mentioned ${e.frequency}x)`);
414
+ const idToName = Object.fromEntries(entityList.map((e) => [String(e.id), e.name]));
415
+ const relLines = (pc.relationships ?? []).map(
416
+ (r) => ` - ${idToName[String(r.source_entity_id)] ?? "?"} -> ${r.label} -> ${idToName[String(r.target_entity_id)] ?? "?"}`
417
+ );
418
+ return `COMMUNITY ${idx}:
419
+ Entities (${entityList.length}):
420
+ ` + entityLines.join("\n") + `
421
+ Relationships (${relLines.length}):
422
+ ` + (relLines.length ? relLines.join("\n") : " None");
423
+ });
424
+ const prompt = "Summarize each community below in 2-4 sentences.\n\n" + blocks.join("\n\n") + '\n\nReturn a JSON array with one summary string per community, in order: ["summary 0", "summary 1", ...]. Return ONLY the JSON array.';
425
+ try {
426
+ const { callLlm: callLlm2 } = await Promise.resolve().then(() => (init_llm(), llm_exports));
427
+ const out = await callLlm2(config.graph.extractionLlm, {
428
+ system: SUMMARY_SYSTEM,
429
+ user: prompt,
430
+ jsonMode: true
431
+ });
432
+ const raw = Array.isArray(out) ? out[0] : out.text ?? String(out);
433
+ let summaries = [];
434
+ try {
435
+ summaries = JSON.parse(jsonrepair.jsonrepair(String(raw)));
436
+ } catch {
437
+ summaries = [];
438
+ }
439
+ if (!Array.isArray(summaries)) summaries = [];
440
+ while (summaries.length < pending.length) summaries.push(null);
441
+ return summaries.slice(0, pending.length).map((s) => typeof s === "string" && s.trim() ? s : null);
442
+ } catch (exc) {
443
+ console.warn("community summarization failed:", exc);
444
+ return pending.map(() => null);
445
+ }
446
+ }
447
+ async function detectCommunities(opts) {
448
+ const stats = { communities_detected: 0, communities_summarized: 0, primary_communities: 0 };
449
+ const communities = await detectWithPython(opts.pool);
450
+ stats.communities_detected = communities.length;
451
+ const pending = [];
452
+ for (const comm of communities) {
453
+ const entityIds = comm.entity_ids;
454
+ if (entityIds.length < MIN_COMMUNITY_SIZE) continue;
455
+ const entities = (await opts.pool.query(`SELECT * FROM context_engine_entities WHERE id = ANY($1::uuid[])`, [entityIds])).rows;
456
+ if (entities.length < MIN_COMMUNITY_SIZE) continue;
457
+ const rels = (await opts.pool.query(
458
+ `SELECT * FROM context_engine_entity_relationships
459
+ WHERE source_entity_id = ANY($1::uuid[]) AND target_entity_id = ANY($1::uuid[])
460
+ LIMIT 50`,
461
+ [entityIds]
462
+ )).rows;
463
+ pending.push({
464
+ entities,
465
+ entity_ids: entityIds,
466
+ entity_count: entityIds.length,
467
+ rel_count: rels.length,
468
+ relationships: rels,
469
+ level: comm.level ?? 0
470
+ });
471
+ }
472
+ if (!pending.length) return stats;
473
+ const summaries = await summarizeCommunities(pending, opts.config);
474
+ const embeddings = summaries.map(() => null);
475
+ const toEmbed = summaries.map((s, i) => [i, s]).filter((x) => Boolean(x[1]));
476
+ if (toEmbed.length) {
477
+ try {
478
+ const result = await opts.embedder.embed(
479
+ toEmbed.map(([, s]) => s),
480
+ { kind: "document" }
481
+ );
482
+ const vectors = Array.isArray(result) ? result[0] : result.vectors ?? [];
483
+ toEmbed.forEach(([i], idx) => {
484
+ embeddings[i] = vectors[idx] ? [...vectors[idx]] : null;
485
+ });
486
+ } catch (exc) {
487
+ if (opts.hooks) emitError(opts.hooks, exc, { stage: "community_embedding" });
488
+ else console.warn("community embedding failed:", exc);
489
+ }
490
+ }
491
+ await opts.pool.query(`DELETE FROM context_engine_communities`);
492
+ let primary = 0;
493
+ for (let i = 0; i < pending.length; i++) {
494
+ const pc = pending[i];
495
+ const embedding = embeddings[i];
496
+ await opts.pool.query(
497
+ `INSERT INTO context_engine_communities
498
+ (id, level, entity_ids, entity_count, relationship_count, summary, embedding)
499
+ VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7::vector)`,
500
+ [
501
+ crypto.randomUUID(),
502
+ pc.level,
503
+ JSON.stringify(pc.entity_ids.map(String)),
504
+ pc.entity_count,
505
+ pc.rel_count,
506
+ summaries[i],
507
+ embedding ? `[${embedding.join(",")}]` : null
508
+ ]
509
+ );
510
+ if (pc.level === 0) primary += 1;
511
+ }
512
+ stats.communities_summarized = pending.length;
513
+ stats.primary_communities = primary;
514
+ return stats;
515
+ }
516
+ exports.MAX_HIERARCHY_LEVELS = void 0; var MIN_COMMUNITY_SIZE, MAX_SUMMARY_ENTITIES, LOUVAIN_SEED, SUMMARY_SYSTEM;
517
+ var init_communities = __esm({
518
+ "src/graph/communities.ts"() {
519
+ init_extras();
520
+ init_hooks();
521
+ exports.MAX_HIERARCHY_LEVELS = 3;
522
+ MIN_COMMUNITY_SIZE = 3;
523
+ MAX_SUMMARY_ENTITIES = 30;
524
+ LOUVAIN_SEED = 42;
525
+ SUMMARY_SYSTEM = "You are a knowledge graph analyst. Be factual.";
526
+ }
527
+ });
528
+ function normalizeEntityName(name) {
529
+ return (name || "").toLowerCase().trim().replace(/ {2}/g, " ");
530
+ }
531
+ function sharedSignificantTokens(a, b, minLen = 4) {
532
+ const tokensA = new Set(a.split(/\s+/).filter((t) => t.length >= minLen));
533
+ const tokensB = b.split(/\s+/).filter((t) => t.length >= minLen);
534
+ return tokensB.some((t) => tokensA.has(t));
535
+ }
536
+ function buildPrompt(fullText) {
537
+ return `Extract ALL entities and relationships from the following document text.
538
+
539
+ DOCUMENT TEXT:
540
+ ${fullText}
541
+
542
+ ENTITY TYPES: PERSON, ORG, PRODUCT, LOCATION, REFERENCE, TEMPORAL, CONCEPT
543
+
544
+ RELATIONSHIP CATEGORIES: HIERARCHICAL, MEMBERSHIP, CREATION, TEMPORAL, SPATIAL, REFERENCE, FUNCTIONAL, QUANTITATIVE
545
+
546
+ OUTPUT FORMAT (STRICT JSON):
547
+ {
548
+ "entities": [
549
+ {"name": "exact text", "type": "PERSON|ORG|PRODUCT|LOCATION|REFERENCE|TEMPORAL|CONCEPT"}
550
+ ],
551
+ "relationships": [
552
+ {"source": "entity name", "target": "entity name", "category": "CATEGORY", "label": "verb", "evidence": "brief quote"}
553
+ ]
554
+ }
555
+
556
+ RULES:
557
+ - Extract EVERY meaningful entity (people, organizations, products, locations, codes, dates, concepts)
558
+ - Choose the MOST SPECIFIC type; only use CONCEPT when no other type fits
559
+ - Be exhaustive but do NOT invent entities not present in the text
560
+ - Deduplicate: each entity appears once
561
+ - Relationships: source/target MUST be entities you extracted; category MUST be one of the 8
562
+ - Return ONLY the JSON object
563
+ `;
564
+ }
565
+ function safeJsonParse(text, expected) {
566
+ const stripped = text.trim();
567
+ try {
568
+ const result = JSON.parse(stripped);
569
+ if (expected === "object" && result && typeof result === "object" && !Array.isArray(result))
570
+ return result;
571
+ if (expected === "array" && Array.isArray(result)) ;
572
+ } catch {
573
+ }
574
+ const repaired = JSON.parse(jsonrepair.jsonrepair(stripped));
575
+ return repaired;
576
+ }
577
+ async function resolveEntity(db, _name, normalizedName, entityType) {
578
+ const exact = await db.query(`SELECT * FROM context_engine_entities WHERE normalized_name = $1 LIMIT 1`, [
579
+ normalizedName
580
+ ]);
581
+ if (exact.rows[0]) return exact.rows[0];
582
+ const trigram = await db.query(
583
+ `SELECT id FROM context_engine_entities
584
+ WHERE similarity(normalized_name, $1) > 0.75
585
+ ORDER BY similarity(normalized_name, $1) DESC LIMIT 1`,
586
+ [normalizedName]
587
+ );
588
+ if (trigram.rows[0]) {
589
+ const row = await db.query(`SELECT * FROM context_engine_entities WHERE id = $1`, [trigram.rows[0].id]);
590
+ return row.rows[0] ?? null;
591
+ }
592
+ const typed = await db.query(
593
+ `SELECT id, normalized_name FROM context_engine_entities
594
+ WHERE type = $2 AND similarity(normalized_name, $1) > 0.45
595
+ ORDER BY similarity(normalized_name, $1) DESC LIMIT 5`,
596
+ [normalizedName, entityType]
597
+ );
598
+ for (const row of typed.rows) {
599
+ if (sharedSignificantTokens(normalizedName, String(row.normalized_name))) {
600
+ const full = await db.query(`SELECT * FROM context_engine_entities WHERE id = $1`, [row.id]);
601
+ return full.rows[0] ?? null;
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ async function extractFromText(fullText, config) {
607
+ try {
608
+ const llm = config.graph.extractionLlm;
609
+ if (!llm) return { entities: [], relationships: [], tokens: {} };
610
+ const { callLlm: callLlm2 } = await Promise.resolve().then(() => (init_llm(), llm_exports));
611
+ const out = await callLlm2(llm, {
612
+ system: EXTRACTION_SYSTEM,
613
+ user: buildPrompt(fullText),
614
+ jsonMode: true
615
+ });
616
+ const raw = Array.isArray(out) ? out[0] : out.text ?? String(out);
617
+ const tokens = (Array.isArray(out) ? out[1] : out.tokens) ?? {};
618
+ let result = {};
619
+ try {
620
+ result = safeJsonParse(String(raw), "object") ?? {};
621
+ } catch {
622
+ result = {};
623
+ }
624
+ const entities = (result.entities ?? []).filter(
625
+ (e) => Boolean(e && typeof e === "object" && e.name && e.type)
626
+ );
627
+ const relationships = (result.relationships ?? []).filter(
628
+ (r) => Boolean(
629
+ r && typeof r === "object" && r.source && r.target
630
+ )
631
+ );
632
+ return { entities, relationships, tokens };
633
+ } catch (exc) {
634
+ console.error("entity extraction LLM call failed:", exc);
635
+ return { entities: [], relationships: [], tokens: {} };
636
+ }
637
+ }
638
+ async function loadDocumentChunks(pool, documentId) {
639
+ const result = await pool.query(
640
+ `SELECT id, text, idx, source_id, meta_data FROM context_engine_chunks WHERE document_id = $1 ORDER BY idx`,
641
+ [documentId]
642
+ );
643
+ return result.rows.map((c) => ({
644
+ id: c.id,
645
+ text: c.text || "",
646
+ text_lower: String(c.text || "").toLowerCase(),
647
+ idx: c.idx || 0,
648
+ source_id: c.source_id,
649
+ token_count: c.meta_data?.token_count ?? Math.floor(String(c.text || "").length / 4)
650
+ }));
651
+ }
652
+ async function persistExtraction(pool, chunkSnaps, extracted) {
653
+ const stats = { entities_created: 0, entities_found: 0, relationships_created: 0 };
654
+ const byNorm = /* @__PURE__ */ new Map();
655
+ const client = await pool.connect();
656
+ try {
657
+ await client.query("BEGIN");
658
+ for (const entityData of extracted.entities) {
659
+ const normalized = normalizeEntityName(String(entityData.name));
660
+ if (!normalized) continue;
661
+ const entityType = String(entityData.type);
662
+ let entity = await resolveEntity(client, String(entityData.name), normalized, entityType);
663
+ if (entity) {
664
+ await client.query(
665
+ `UPDATE context_engine_entities SET frequency = COALESCE(frequency, 1) + 1,
666
+ name = CASE WHEN length($2) > length(name) THEN $2 ELSE name END, updated_at = now()
667
+ WHERE id = $1`,
668
+ [entity.id, entityData.name]
669
+ );
670
+ entity = (await client.query(`SELECT * FROM context_engine_entities WHERE id = $1`, [entity.id])).rows[0];
671
+ } else {
672
+ const id = crypto.randomUUID();
673
+ await client.query(
674
+ `INSERT INTO context_engine_entities (id, name, normalized_name, type, frequency)
675
+ VALUES ($1, $2, $3, $4, 1)`,
676
+ [id, entityData.name, normalized, entityType]
677
+ );
678
+ entity = (await client.query(`SELECT * FROM context_engine_entities WHERE id = $1`, [id])).rows[0];
679
+ stats.entities_created += 1;
680
+ }
681
+ byNorm.set(normalized, entity);
682
+ byNorm.set(String(entity.normalized_name), entity);
683
+ const nameLower = String(entityData.name).toLowerCase();
684
+ for (const snap of chunkSnaps) {
685
+ if (nameLower && String(snap.text_lower).includes(nameLower)) {
686
+ const exists = await client.query(
687
+ `SELECT 1 FROM context_engine_chunk_entities WHERE chunk_id = $1 AND entity_id = $2`,
688
+ [snap.id, entity.id]
689
+ );
690
+ if (!exists.rows.length) {
691
+ await client.query(
692
+ `INSERT INTO context_engine_chunk_entities (chunk_id, entity_id) VALUES ($1, $2)`,
693
+ [snap.id, entity.id]
694
+ );
695
+ stats.entities_found += 1;
696
+ }
697
+ }
698
+ }
699
+ }
700
+ for (const rel of extracted.relationships) {
701
+ const sourceNorm = normalizeEntityName(String(rel.source ?? ""));
702
+ const targetNorm = normalizeEntityName(String(rel.target ?? ""));
703
+ const category = String(rel.category ?? "").toUpperCase();
704
+ const label = String(rel.label ?? "");
705
+ const evidence = String(rel.evidence ?? "");
706
+ if (!RELATIONSHIP_CATEGORIES.has(category)) continue;
707
+ let src = byNorm.get(sourceNorm);
708
+ let tgt = byNorm.get(targetNorm);
709
+ if (!src) {
710
+ src = (await client.query(`SELECT * FROM context_engine_entities WHERE normalized_name = $1`, [sourceNorm])).rows[0];
711
+ }
712
+ if (!tgt) {
713
+ tgt = (await client.query(`SELECT * FROM context_engine_entities WHERE normalized_name = $1`, [targetNorm])).rows[0];
714
+ }
715
+ if (!src || !tgt) continue;
716
+ const exists = await client.query(
717
+ `SELECT 1 FROM context_engine_entity_relationships
718
+ WHERE source_entity_id = $1 AND target_entity_id = $2 AND category = $3 AND label = $4`,
719
+ [src.id, tgt.id, category, label]
720
+ );
721
+ if (!exists.rows.length) {
722
+ await client.query(
723
+ `INSERT INTO context_engine_entity_relationships
724
+ (id, source_entity_id, target_entity_id, category, label, evidence)
725
+ VALUES ($1, $2, $3, $4, $5, $6)`,
726
+ [crypto.randomUUID(), src.id, tgt.id, category, label, evidence.slice(0, 200)]
727
+ );
728
+ stats.relationships_created += 1;
729
+ }
730
+ }
731
+ await client.query("COMMIT");
732
+ } catch (e) {
733
+ await client.query("ROLLBACK");
734
+ throw e;
735
+ } finally {
736
+ client.release();
737
+ }
738
+ return stats;
739
+ }
740
+ async function syncToNeo4j(pool, documentId, chunkSnaps, graphStore) {
741
+ const chunkRows = chunkSnaps.map((s) => ({
742
+ id: String(s.id),
743
+ document_id: String(documentId),
744
+ source_id: s.source_id,
745
+ text_preview: String(s.text).slice(0, 200),
746
+ position: s.idx,
747
+ token_count: s.token_count
748
+ }));
749
+ try {
750
+ await graphStore.connect();
751
+ const entities = (await pool.query(`SELECT * FROM context_engine_entities`)).rows;
752
+ const entityById = new Map(entities.map((e) => [String(e.id), e]));
753
+ const entityRows = entities.map((e) => ({
754
+ name: e.name,
755
+ normalized_name: e.normalized_name,
756
+ entity_type: e.type
757
+ }));
758
+ const ids = chunkSnaps.map((s) => s.id);
759
+ const links = ids.length ? (await pool.query(`SELECT * FROM context_engine_chunk_entities WHERE chunk_id = ANY($1::uuid[])`, [
760
+ ids
761
+ ])).rows : [];
762
+ const mentionRows = links.filter((l) => entityById.has(String(l.entity_id))).map((l) => ({
763
+ chunk_id: String(l.chunk_id),
764
+ entity_name: entityById.get(String(l.entity_id)).normalized_name
765
+ }));
766
+ const rels = (await pool.query(`SELECT * FROM context_engine_entity_relationships`)).rows;
767
+ const relRows = [];
768
+ for (const r of rels) {
769
+ const src = entityById.get(String(r.source_entity_id));
770
+ const tgt = entityById.get(String(r.target_entity_id));
771
+ if (src && tgt) {
772
+ relRows.push({
773
+ source_name: src.normalized_name,
774
+ target_name: tgt.normalized_name,
775
+ category: r.category,
776
+ label: r.label,
777
+ evidence: r.evidence,
778
+ chunk_id: r.chunk_id ? String(r.chunk_id) : null
779
+ });
780
+ }
781
+ }
782
+ await graphStore.upsertChunksBatch(chunkRows);
783
+ await graphStore.linkSequentialChunks(String(documentId));
784
+ await graphStore.upsertEntitiesBatch(entityRows);
785
+ await graphStore.linkChunksToEntitiesBatch(mentionRows);
786
+ await graphStore.upsertEntityRelationshipsBatch(relRows);
787
+ } catch (exc) {
788
+ console.warn("Neo4j sync failed (continuing on PG mirror):", exc);
789
+ }
790
+ }
791
+ async function extractEntitiesForDocument(documentId, opts) {
792
+ const chunkSnaps = await loadDocumentChunks(opts.pool, documentId);
793
+ const stats = {
794
+ chunk_count: chunkSnaps.length,
795
+ entities_created: 0,
796
+ entities_found: 0,
797
+ relationships_created: 0,
798
+ provider_tokens: {}
799
+ };
800
+ if (!chunkSnaps.length) return stats;
801
+ const batches = [];
802
+ for (let i = 0; i < chunkSnaps.length; i += exports.CHUNKS_PER_BATCH) {
803
+ batches.push(chunkSnaps.slice(i, i + exports.CHUNKS_PER_BATCH));
804
+ }
805
+ let llmInput = 0;
806
+ let llmOutput = 0;
807
+ for (const batch of batches) {
808
+ const fullText = batch.map((s) => String(s.text)).join("\n\n");
809
+ const extracted = await extractFromText(fullText, opts.config);
810
+ llmInput += Number(extracted.tokens.input ?? 0);
811
+ llmOutput += Number(extracted.tokens.output ?? 0);
812
+ const persisted = await persistExtraction(opts.pool, batch, extracted);
813
+ stats.entities_created = Number(stats.entities_created) + persisted.entities_created;
814
+ stats.entities_found = Number(stats.entities_found) + persisted.entities_found;
815
+ stats.relationships_created = Number(stats.relationships_created) + persisted.relationships_created;
816
+ }
817
+ const tokens = stats.provider_tokens;
818
+ if (llmInput) tokens.graph_llm_input = llmInput;
819
+ if (llmOutput) tokens.graph_llm_output = llmOutput;
820
+ if (opts.graphStore) await syncToNeo4j(opts.pool, documentId, chunkSnaps, opts.graphStore);
821
+ return stats;
822
+ }
823
+ exports.ENTITY_TYPES = void 0; var RELATIONSHIP_CATEGORIES; exports.CHUNKS_PER_BATCH = void 0; var EXTRACTION_SYSTEM;
824
+ var init_entities = __esm({
825
+ "src/graph/entities.ts"() {
826
+ exports.ENTITY_TYPES = [
827
+ "PERSON",
828
+ "ORG",
829
+ "PRODUCT",
830
+ "LOCATION",
831
+ "REFERENCE",
832
+ "TEMPORAL",
833
+ "CONCEPT"
834
+ ];
835
+ RELATIONSHIP_CATEGORIES = /* @__PURE__ */ new Set([
836
+ "HIERARCHICAL",
837
+ "MEMBERSHIP",
838
+ "CREATION",
839
+ "TEMPORAL",
840
+ "SPATIAL",
841
+ "REFERENCE",
842
+ "FUNCTIONAL",
843
+ "QUANTITATIVE"
844
+ ]);
845
+ exports.CHUNKS_PER_BATCH = 11;
846
+ EXTRACTION_SYSTEM = "You are a STRICT JSON entity and relationship extraction engine.";
847
+ }
848
+ });
849
+
850
+ // src/graph/schema.ts
851
+ async function ensureSchema(driver, database) {
852
+ const session = driver.session({
853
+ database
854
+ });
855
+ try {
856
+ for (const stmt of [...CONSTRAINTS, ...INDEXES]) {
857
+ try {
858
+ await session.run(stmt);
859
+ } catch (exc) {
860
+ console.error("Neo4j schema statement FAILED:", stmt.split(" ")[2], exc);
861
+ }
862
+ }
863
+ } finally {
864
+ await session.close();
865
+ }
866
+ }
867
+ var CONSTRAINTS, INDEXES;
868
+ var init_schema = __esm({
869
+ "src/graph/schema.ts"() {
870
+ CONSTRAINTS = [
871
+ "CREATE CONSTRAINT ce_chunk_unique IF NOT EXISTS FOR (c:Chunk) REQUIRE c.id IS UNIQUE",
872
+ "CREATE CONSTRAINT ce_entity_unique IF NOT EXISTS FOR (e:Entity) REQUIRE e.normalized_name IS UNIQUE"
873
+ ];
874
+ INDEXES = [
875
+ "CREATE INDEX ce_entity_type_idx IF NOT EXISTS FOR (e:Entity) ON (e.type)",
876
+ "CREATE INDEX ce_chunk_source_idx IF NOT EXISTS FOR (c:Chunk) ON (c.source_id)",
877
+ "CREATE INDEX ce_chunk_docid_idx IF NOT EXISTS FOR (c:Chunk) ON (c.document_id)"
878
+ ];
879
+ }
880
+ });
881
+
882
+ // src/graph/neo4j-client.ts
883
+ var neo4j_client_exports = {};
884
+ __export(neo4j_client_exports, {
885
+ GraphStore: () => exports.GraphStore
886
+ });
887
+ var SYNC_BATCH_ROWS, MAX_EXPANSION; exports.GraphStore = void 0;
888
+ var init_neo4j_client = __esm({
889
+ "src/graph/neo4j-client.ts"() {
890
+ init_extras();
891
+ init_hooks();
892
+ init_schema();
893
+ SYNC_BATCH_ROWS = 1e3;
894
+ MAX_EXPANSION = 500;
895
+ exports.GraphStore = class {
896
+ cfg;
897
+ hooks;
898
+ database;
899
+ driver = null;
900
+ schemaReady = false;
901
+ cache = /* @__PURE__ */ new Map();
902
+ generation = 0;
903
+ constructor(cfg, opts = {}) {
904
+ this.cfg = cfg;
905
+ this.hooks = opts.hooks;
906
+ this.database = cfg.neo4jDatabase;
907
+ }
908
+ async connect() {
909
+ if (this.driver) return;
910
+ const neo4j = await requireExtra("neo4j-driver", "graph", "graph features");
911
+ const ns = neo4j.default ?? neo4j;
912
+ const driverFn = ns.driver;
913
+ const auth = ns.auth;
914
+ if (!driverFn || !auth) throw new Error("neo4j-driver export shape is unexpected");
915
+ const driver = driverFn(this.cfg.neo4jUri, auth.basic(this.cfg.neo4jUser, this.cfg.neo4jPassword));
916
+ this.driver = driver;
917
+ if (!this.schemaReady) {
918
+ await ensureSchema(driver, this.database);
919
+ this.schemaReady = true;
920
+ }
921
+ }
922
+ async close() {
923
+ if (this.driver) {
924
+ await this.driver.close();
925
+ this.driver = null;
926
+ this.schemaReady = false;
927
+ }
928
+ }
929
+ async ping(timeout = 2e3) {
930
+ try {
931
+ await this.connect();
932
+ await Promise.race([
933
+ this.pingInternal(),
934
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeout))
935
+ ]);
936
+ return true;
937
+ } catch (exc) {
938
+ this.report(exc, { service: "neo4j", operation: "ping" });
939
+ return false;
940
+ }
941
+ }
942
+ async pingInternal() {
943
+ const session = this.driver.session({ database: this.database });
944
+ try {
945
+ await session.run("RETURN 1 AS ping");
946
+ } finally {
947
+ await session.close();
948
+ }
949
+ }
950
+ report(exc, ctx) {
951
+ if (this.hooks) emitError(this.hooks, exc, ctx);
952
+ else console.error("neo4j error:", exc, "| ctx=", ctx);
953
+ }
954
+ cacheGet(key) {
955
+ const entry = this.cache.get(key);
956
+ if (!entry) return null;
957
+ if (entry.expiresAt > Date.now() / 1e3) return entry.value;
958
+ this.cache.delete(key);
959
+ return null;
960
+ }
961
+ cacheSet(key, value, ttl = 86400) {
962
+ this.cache.set(key, { expiresAt: Date.now() / 1e3 + ttl, value });
963
+ }
964
+ async upsertChunksBatch(rows) {
965
+ const query = `
966
+ UNWIND $rows AS row
967
+ MERGE (c:Chunk {id: row.id})
968
+ SET c.document_id = row.document_id,
969
+ c.source_id = row.source_id,
970
+ c.text_preview = row.text_preview,
971
+ c.position = row.position,
972
+ c.token_count = row.token_count,
973
+ c.updated_at = datetime()
974
+ `;
975
+ return this.runBatched(query, rows);
976
+ }
977
+ async upsertEntitiesBatch(rows) {
978
+ const query = `
979
+ UNWIND $rows AS row
980
+ MERGE (e:Entity {normalized_name: row.normalized_name})
981
+ SET e.name = row.name,
982
+ e.type = row.entity_type,
983
+ e.created_at = coalesce(e.created_at, datetime()),
984
+ e.updated_at = datetime()
985
+ `;
986
+ return this.runBatched(query, rows);
987
+ }
988
+ async linkChunksToEntitiesBatch(rows) {
989
+ const query = `
990
+ UNWIND $rows AS row
991
+ MATCH (c:Chunk {id: row.chunk_id})
992
+ MATCH (e:Entity {normalized_name: row.entity_name})
993
+ MERGE (c)-[:MENTIONS]->(e)
994
+ `;
995
+ return this.runBatched(query, rows);
996
+ }
997
+ async upsertEntityRelationshipsBatch(rows) {
998
+ const query = `
999
+ UNWIND $rows AS row
1000
+ MATCH (src:Entity {normalized_name: row.source_name})
1001
+ MATCH (tgt:Entity {normalized_name: row.target_name})
1002
+ MERGE (src)-[r:RELATES_TO {category: row.category, label: row.label}]->(tgt)
1003
+ SET r.evidence = row.evidence,
1004
+ r.chunk_id = row.chunk_id,
1005
+ r.updated_at = datetime()
1006
+ `;
1007
+ return this.runBatched(query, rows);
1008
+ }
1009
+ async linkSequentialChunks(documentId) {
1010
+ const query = `
1011
+ MATCH (c:Chunk {document_id: $document_id})
1012
+ WITH c ORDER BY c.position
1013
+ WITH COLLECT(c) AS chunks
1014
+ UNWIND RANGE(0, SIZE(chunks)-2) AS i
1015
+ WITH chunks[i] AS curr, chunks[i+1] AS next
1016
+ MERGE (curr)-[:NEXT]->(next)
1017
+ `;
1018
+ await this.connect();
1019
+ const session = this.driver.session({ database: this.database });
1020
+ try {
1021
+ await session.run(query, { document_id: String(documentId) });
1022
+ } finally {
1023
+ await session.close();
1024
+ }
1025
+ this.generation += 1;
1026
+ }
1027
+ async runBatched(query, rows) {
1028
+ if (!rows.length) return 0;
1029
+ await this.connect();
1030
+ let done = 0;
1031
+ const session = this.driver.session({ database: this.database });
1032
+ try {
1033
+ for (let i = 0; i < rows.length; i += SYNC_BATCH_ROWS) {
1034
+ const batch = rows.slice(i, i + SYNC_BATCH_ROWS);
1035
+ await session.run(query, { rows: batch });
1036
+ done += batch.length;
1037
+ }
1038
+ } finally {
1039
+ await session.close();
1040
+ }
1041
+ this.generation += 1;
1042
+ return done;
1043
+ }
1044
+ async deleteSourceData(sourceId) {
1045
+ await this.connect();
1046
+ const session = this.driver.session({ database: this.database });
1047
+ try {
1048
+ const result = await session.run(
1049
+ "MATCH (c:Chunk {source_id: $source_id}) DETACH DELETE c RETURN count(c) AS n",
1050
+ { source_id: String(sourceId) }
1051
+ );
1052
+ const deleted = Number(result.records[0]?.get("n") ?? 0);
1053
+ this.generation += 1;
1054
+ return { chunks_deleted: deleted };
1055
+ } finally {
1056
+ await session.close();
1057
+ }
1058
+ }
1059
+ expansionCacheKey(queryEntities, maxDepth, chunkIds) {
1060
+ const chunkDigest = crypto.createHash("md5").update([...chunkIds].sort().join(",")).digest("hex");
1061
+ const keyContent = `g${this.generation}:${JSON.stringify([...queryEntities].sort())}:${maxDepth}:${chunkDigest}`;
1062
+ return `expansion:${crypto.createHash("md5").update(keyContent).digest("hex")}`;
1063
+ }
1064
+ async expandChunkSet(chunkIds, queryEntities, maxDepth = 2) {
1065
+ if (!chunkIds.length) return [];
1066
+ const depth = Math.max(1, Math.min(Math.trunc(maxDepth), 5));
1067
+ const cacheKey = this.expansionCacheKey(queryEntities, depth, chunkIds);
1068
+ const cached = this.cacheGet(cacheKey);
1069
+ if (Array.isArray(cached)) return cached;
1070
+ const query = `
1071
+ MATCH (c:Chunk)
1072
+ WHERE c.id IN $chunk_ids
1073
+ WITH COLLECT(DISTINCT c) AS seeds
1074
+
1075
+ CALL {
1076
+ WITH seeds
1077
+ UNWIND seeds AS c
1078
+ OPTIONAL MATCH (c)-[:MENTIONS]->(e:Entity)<-[:MENTIONS]-(related:Chunk)
1079
+ WHERE e.normalized_name IN $query_entities
1080
+ RETURN COLLECT(DISTINCT related.id) AS related_ids
1081
+ }
1082
+ CALL {
1083
+ WITH seeds
1084
+ UNWIND seeds AS c
1085
+ OPTIONAL MATCH (c)-[:NEXT*1..${depth}]-(sequential:Chunk)
1086
+ RETURN COLLECT(DISTINCT sequential.id) AS sequential_ids
1087
+ }
1088
+
1089
+ WITH [s IN seeds | s.id] + related_ids + sequential_ids AS all_ids
1090
+ UNWIND all_ids AS chunk_id
1091
+ WITH chunk_id WHERE chunk_id IS NOT NULL
1092
+ RETURN DISTINCT chunk_id
1093
+ LIMIT $max_results
1094
+ `;
1095
+ await this.connect();
1096
+ const session = this.driver.session({ database: this.database });
1097
+ try {
1098
+ const result = await session.run(query, {
1099
+ chunk_ids: chunkIds,
1100
+ query_entities: queryEntities,
1101
+ max_results: MAX_EXPANSION
1102
+ });
1103
+ const expanded = result.records.map((r) => r.get("chunk_id")).filter((v) => v != null).map(String);
1104
+ if (expanded.length) this.cacheSet(cacheKey, expanded);
1105
+ return expanded;
1106
+ } finally {
1107
+ await session.close();
1108
+ }
1109
+ }
1110
+ async getChunkConnectivityScores(chunkIds) {
1111
+ if (!chunkIds.length) return {};
1112
+ const query = `
1113
+ UNWIND $chunk_ids AS chunk_id
1114
+ MATCH (c:Chunk {id: chunk_id})
1115
+ OPTIONAL MATCH (c)-[out_next:NEXT]->()
1116
+ OPTIONAL MATCH (c)-[out_mentions:MENTIONS]->()
1117
+ OPTIONAL MATCH (c)<-[in_next:NEXT]-()
1118
+ OPTIONAL MATCH (c)<-[in_mentions:MENTIONS]-()
1119
+ WITH c.id AS id,
1120
+ count(DISTINCT out_next) + count(DISTINCT out_mentions) AS outgoing,
1121
+ count(DISTINCT in_next) + count(DISTINCT in_mentions) AS incoming
1122
+ RETURN id, (outgoing + incoming) AS total_connections
1123
+ `;
1124
+ await this.connect();
1125
+ const session = this.driver.session({ database: this.database });
1126
+ try {
1127
+ const result = await session.run(query, { chunk_ids: chunkIds });
1128
+ const records = result.records.map((r) => ({
1129
+ id: String(r.get("id")),
1130
+ total_connections: Number(r.get("total_connections"))
1131
+ }));
1132
+ if (!records.length) return {};
1133
+ const maxConnections = Math.max(...records.map((r) => r.total_connections));
1134
+ if (maxConnections === 0) {
1135
+ return Object.fromEntries(records.map((r) => [r.id, 0.5]));
1136
+ }
1137
+ return Object.fromEntries(
1138
+ records.map((r) => [r.id, Math.min(1, r.total_connections / maxConnections)])
1139
+ );
1140
+ } finally {
1141
+ await session.close();
1142
+ }
1143
+ }
1144
+ };
1145
+ }
1146
+ });
1147
+
1148
+ // src/graph/retrieval.ts
1149
+ var retrieval_exports = {};
1150
+ __export(retrieval_exports, {
1151
+ buildGraphRanked: () => buildGraphRanked,
1152
+ corpusIsAclUniform: () => corpusIsAclUniform,
1153
+ shouldUseCommunitySummaries: () => shouldUseCommunitySummaries
1154
+ });
1155
+ function vecLiteral(vector) {
1156
+ return `[${vector.map((x) => Number(x)).join(",")}]`;
1157
+ }
1158
+ async function corpusIsAclUniform(pool, sourceIds) {
1159
+ const result = await pool.query(
1160
+ `SELECT EXISTS(SELECT 1 FROM context_engine_chunks c
1161
+ WHERE c.acl IS NOT NULL AND ($1::text[] IS NULL OR c.source_id = ANY($1::text[]))) AS has_acl`,
1162
+ [sourceIds]
1163
+ );
1164
+ return !result.rows[0]?.has_acl;
1165
+ }
1166
+ async function shouldUseCommunitySummaries(pool, opts) {
1167
+ if (opts.principals == null) return true;
1168
+ return corpusIsAclUniform(pool, opts.sourceIds);
1169
+ }
1170
+ async function deriveQueryEntities(pool, chunkIds, opts) {
1171
+ if (!chunkIds.length) return [];
1172
+ const result = await pool.query(
1173
+ `SELECT e.normalized_name, e.name, e.type, COUNT(*) AS freq
1174
+ FROM context_engine_chunk_entities ce
1175
+ JOIN context_engine_entities e ON e.id = ce.entity_id
1176
+ JOIN context_engine_chunks c ON c.id = ce.chunk_id
1177
+ WHERE ce.chunk_id = ANY($1::uuid[]) ${SCOPE}
1178
+ GROUP BY e.normalized_name, e.name, e.type
1179
+ ORDER BY freq DESC LIMIT $4`,
1180
+ [chunkIds, opts.sourceIds, opts.principals, ENTITY_LIMIT]
1181
+ );
1182
+ return result.rows.map((r) => ({
1183
+ normalized_name: r.normalized_name,
1184
+ name: r.name,
1185
+ type: r.type
1186
+ }));
1187
+ }
1188
+ async function computeVectorScores(pool, chunkIds, vector) {
1189
+ if (!chunkIds.length || !vector) return {};
1190
+ const result = await pool.query(
1191
+ `SELECT c.id::text, 1 - (c.embedding <=> CAST($2 AS vector))::float AS sim
1192
+ FROM context_engine_chunks c
1193
+ WHERE c.id = ANY($1::uuid[]) AND c.embedding IS NOT NULL`,
1194
+ [chunkIds, vecLiteral(vector)]
1195
+ );
1196
+ let scores = Object.fromEntries(result.rows.map((r) => [String(r.id), Number(r.sim)]));
1197
+ const vs = Object.values(scores);
1198
+ if (vs.length) {
1199
+ const lo = Math.min(...vs);
1200
+ const hi = Math.max(...vs);
1201
+ if (hi > lo)
1202
+ scores = Object.fromEntries(Object.entries(scores).map(([k, v]) => [k, (v - lo) / (hi - lo)]));
1203
+ }
1204
+ return scores;
1205
+ }
1206
+ async function computeRelationshipScores(pool, chunkIds, queryEntityNames) {
1207
+ if (!chunkIds.length || !queryEntityNames.length) return {};
1208
+ const result = await pool.query(
1209
+ `SELECT r.chunk_id::text, COUNT(*) AS n
1210
+ FROM context_engine_entity_relationships r
1211
+ JOIN context_engine_entities se ON se.id = r.source_entity_id
1212
+ JOIN context_engine_entities te ON te.id = r.target_entity_id
1213
+ WHERE r.chunk_id = ANY($1::uuid[])
1214
+ AND (se.normalized_name = ANY($2::text[]) OR te.normalized_name = ANY($2::text[]))
1215
+ GROUP BY r.chunk_id`,
1216
+ [chunkIds, queryEntityNames]
1217
+ );
1218
+ const counts = Object.fromEntries(result.rows.map((r) => [String(r.chunk_id), Number(r.n)]));
1219
+ if (!Object.keys(counts).length) return {};
1220
+ const mx = Math.max(...Object.values(counts));
1221
+ return mx ? Object.fromEntries(Object.entries(counts).map(([cid, n]) => [cid, n / mx])) : {};
1222
+ }
1223
+ async function computeCommunityScores(pool, chunkIds, vector, opts) {
1224
+ if (!opts.useSummaries || !chunkIds.length || !vector) return {};
1225
+ const commRows = await pool.query(
1226
+ `SELECT entity_ids, 1 - (embedding <=> CAST($1 AS vector))::float AS sim
1227
+ FROM context_engine_communities WHERE embedding IS NOT NULL
1228
+ ORDER BY embedding <=> CAST($1 AS vector) LIMIT 5`,
1229
+ [vecLiteral(vector)]
1230
+ );
1231
+ if (!commRows.rows.length) return {};
1232
+ const entityScore = {};
1233
+ for (const row of commRows.rows) {
1234
+ const sim = Number(row.sim);
1235
+ if (sim < 0.15) continue;
1236
+ for (const eid of row.entity_ids ?? []) {
1237
+ entityScore[String(eid)] = Math.max(entityScore[String(eid)] ?? 0, sim);
1238
+ }
1239
+ }
1240
+ if (!Object.keys(entityScore).length) return {};
1241
+ const result = await pool.query(
1242
+ `SELECT ce.chunk_id::text, ce.entity_id::text
1243
+ FROM context_engine_chunk_entities ce
1244
+ JOIN context_engine_chunks c ON c.id = ce.chunk_id
1245
+ WHERE ce.chunk_id = ANY($1::uuid[]) AND ce.entity_id = ANY($4::uuid[]) ${SCOPE}`,
1246
+ [chunkIds, opts.sourceIds, opts.principals, Object.keys(entityScore)]
1247
+ );
1248
+ const chunkScores = {};
1249
+ for (const row of result.rows) {
1250
+ chunkScores[row.chunk_id] = Math.max(chunkScores[row.chunk_id] ?? 0, entityScore[row.entity_id] ?? 0);
1251
+ }
1252
+ return chunkScores;
1253
+ }
1254
+ async function vectorSeedIds(pool, vector, opts) {
1255
+ const result = await pool.query(
1256
+ `SELECT c.id::text FROM context_engine_chunks c
1257
+ WHERE c.embedding IS NOT NULL ${SCOPE}
1258
+ ORDER BY c.embedding <=> CAST($1 AS vector) LIMIT $4`,
1259
+ [vecLiteral(vector), opts.sourceIds, opts.principals, SEED_LIMIT]
1260
+ );
1261
+ return result.rows.map((r) => String(r.id));
1262
+ }
1263
+ async function buildGraphRanked(query, opts) {
1264
+ try {
1265
+ const result = await opts.embedder.embed([query], { kind: "query" });
1266
+ const vectors = Array.isArray(result) ? result[0] : result.vectors ?? [];
1267
+ const vector = vectors[0] ? [...vectors[0]] : null;
1268
+ if (!vector) return [];
1269
+ const sourceIds = opts.sourceIds ?? null;
1270
+ const principals = opts.principals ?? null;
1271
+ const seeds = await vectorSeedIds(opts.pool, vector, { sourceIds, principals });
1272
+ if (!seeds.length) return [];
1273
+ const queryEntities = await deriveQueryEntities(opts.pool, seeds.slice(0, TOP_SEEDS_FOR_ENTITIES), {
1274
+ sourceIds,
1275
+ principals
1276
+ });
1277
+ const entityNorms = queryEntities.map((e) => String(e.normalized_name));
1278
+ let expanded = seeds;
1279
+ let connectivity = {};
1280
+ try {
1281
+ await opts.graphStore.connect();
1282
+ expanded = await opts.graphStore.expandChunkSet(seeds, entityNorms, opts.maxDepth ?? 2);
1283
+ connectivity = await opts.graphStore.getChunkConnectivityScores(expanded.length ? expanded : seeds);
1284
+ } catch (exc) {
1285
+ if (opts.hooks) emitError(opts.hooks, exc, { stage: "graph_expand" });
1286
+ else console.warn("graph leg: expansion failed:", exc);
1287
+ expanded = seeds;
1288
+ connectivity = {};
1289
+ }
1290
+ const allIds = expanded.length ? expanded : seeds;
1291
+ const useSummaries = await shouldUseCommunitySummaries(opts.pool, { sourceIds, principals });
1292
+ const vecScores = await computeVectorScores(opts.pool, allIds, vector);
1293
+ const relScores = await computeRelationshipScores(opts.pool, allIds, entityNorms);
1294
+ const commScores = await computeCommunityScores(opts.pool, allIds, vector, {
1295
+ sourceIds,
1296
+ principals,
1297
+ useSummaries
1298
+ });
1299
+ const textRows = await opts.pool.query(
1300
+ `SELECT id::text, lower(coalesce(text,'')) AS t FROM context_engine_chunks WHERE id = ANY($1::uuid[])`,
1301
+ [allIds]
1302
+ );
1303
+ const texts = Object.fromEntries(textRows.rows.map((r) => [String(r.id), String(r.t)]));
1304
+ const weights = {
1305
+ ...DEFAULT_WEIGHTS,
1306
+ ...opts.config.graph.rerankWeights ?? opts.config.graph.rerank_weights ?? {}
1307
+ };
1308
+ const entityNameSet = new Set(entityNorms);
1309
+ const scored = [];
1310
+ for (const cidRaw of allIds) {
1311
+ const cid = String(cidRaw);
1312
+ if (!(cid in texts)) continue;
1313
+ const v = vecScores[cid] ?? 0.5;
1314
+ const chunkText = texts[cid] ?? "";
1315
+ const matches = [...entityNameSet].filter((n) => n && chunkText.includes(n)).length;
1316
+ const e = entityNameSet.size ? Math.min(1, matches / Math.max(1, entityNameSet.size)) : 0;
1317
+ const r = relScores[cid] ?? 0;
1318
+ const cm = commScores[cid] ?? 0;
1319
+ const gc = connectivity[cid] ?? 0.5;
1320
+ const final = v * weights.vector_score + e * weights.entity_match + r * weights.relationship_relevance + cm * weights.community_match + gc * weights.graph_connectivity;
1321
+ scored.push([cid, final]);
1322
+ }
1323
+ scored.sort((a, b) => b[1] - a[1]);
1324
+ return scored.map(([cid]) => cid);
1325
+ } catch (exc) {
1326
+ if (opts.hooks) emitError(opts.hooks, exc, { stage: "graph_embed_query" });
1327
+ else console.warn("graph leg: query embed failed:", exc);
1328
+ return [];
1329
+ }
1330
+ }
1331
+ var DEFAULT_WEIGHTS, SEED_LIMIT, ENTITY_LIMIT, TOP_SEEDS_FOR_ENTITIES, SCOPE;
1332
+ var init_retrieval = __esm({
1333
+ "src/graph/retrieval.ts"() {
1334
+ init_hooks();
1335
+ DEFAULT_WEIGHTS = {
1336
+ vector_score: 0.3,
1337
+ entity_match: 0.3,
1338
+ relationship_relevance: 0.2,
1339
+ community_match: 0.1,
1340
+ graph_connectivity: 0.1
1341
+ };
1342
+ SEED_LIMIT = 200;
1343
+ ENTITY_LIMIT = 30;
1344
+ TOP_SEEDS_FOR_ENTITIES = 20;
1345
+ SCOPE = `
1346
+ AND ($2::text[] IS NULL OR c.source_id = ANY($2::text[]))
1347
+ AND ($3::text[] IS NULL OR c.acl IS NULL OR c.acl && $3::text[])
1348
+ `;
1349
+ }
1350
+ });
1351
+
1352
+ // src/usage.ts
1353
+ function graphUnits(chunkCount, primaryCommunityCount) {
1354
+ return Math.ceil(chunkCount / 8) + primaryCommunityCount;
1355
+ }
1356
+ var init_usage = __esm({
1357
+ "src/usage.ts"() {
1358
+ }
1359
+ });
1360
+
1361
+ // src/graph/stage.ts
1362
+ var stage_exports = {};
1363
+ __export(stage_exports, {
1364
+ runGraphStage: () => runGraphStage
1365
+ });
1366
+ async function runGraphStage(opts) {
1367
+ const entStats = await extractEntitiesForDocument(opts.documentId, {
1368
+ config: opts.config,
1369
+ pool: opts.pool,
1370
+ graphStore: opts.graphStore,
1371
+ hooks: opts.hooks
1372
+ });
1373
+ const commStats = await detectCommunities({
1374
+ config: opts.config,
1375
+ pool: opts.pool,
1376
+ embedder: opts.embedder,
1377
+ hooks: opts.hooks
1378
+ });
1379
+ const chunkCount = Number(entStats.chunk_count ?? 0);
1380
+ const primary = Number(commStats.primary_communities ?? 0);
1381
+ return graphUnits(chunkCount, primary);
1382
+ }
1383
+ var init_stage = __esm({
1384
+ "src/graph/stage.ts"() {
1385
+ init_usage();
1386
+ init_communities();
1387
+ init_entities();
1388
+ }
1389
+ });
1390
+
1391
+ // src/graph/index.ts
1392
+ init_extras();
1393
+ init_communities();
1394
+ init_entities();
1395
+ init_neo4j_client();
1396
+ init_retrieval();
1397
+ async function requireGraph() {
1398
+ await requireExtra("neo4j-driver", "graph", "graph features");
1399
+ }
1400
+ var _requireGraph = requireGraph;
1401
+ async function buildGraphStore(cfg, opts = {}) {
1402
+ await requireGraph();
1403
+ const { GraphStore: GraphStore2 } = await Promise.resolve().then(() => (init_neo4j_client(), neo4j_client_exports));
1404
+ return new GraphStore2(cfg, opts);
1405
+ }
1406
+ async function runGraphStage2(...args) {
1407
+ await requireGraph();
1408
+ const { runGraphStage: impl } = await Promise.resolve().then(() => (init_stage(), stage_exports));
1409
+ return impl(...args);
1410
+ }
1411
+ async function buildGraphRanked2(...args) {
1412
+ await requireGraph();
1413
+ const { buildGraphRanked: impl } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1414
+ return impl(...args);
1415
+ }
1416
+
1417
+ exports._requireGraph = _requireGraph;
1418
+ exports.buildGraphRanked = buildGraphRanked2;
1419
+ exports.buildGraphStore = buildGraphStore;
1420
+ exports.corpusIsAclUniform = corpusIsAclUniform;
1421
+ exports.detectCommunities = detectCommunities;
1422
+ exports.normalizeEntityName = normalizeEntityName;
1423
+ exports.requireGraph = requireGraph;
1424
+ exports.resolveEntity = resolveEntity;
1425
+ exports.runGraphStage = runGraphStage2;
1426
+ exports.shouldUseCommunitySummaries = shouldUseCommunitySummaries;
1427
+ //# sourceMappingURL=index.cjs.map
1428
+ //# sourceMappingURL=index.cjs.map