@equationalapplications/core-llm-wiki 4.15.3 → 4.16.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/dist/testing.js CHANGED
@@ -796,6 +796,11 @@ After running the migration SQL, restart your application.`
796
796
  }
797
797
  };
798
798
 
799
+ // src/utils/ontology.ts
800
+ function normalizeTitleKey(title) {
801
+ return title.trim().toLowerCase().replace(/\s+/g, " ");
802
+ }
803
+
799
804
  // src/utils/ids.ts
800
805
  function generateId(prefix = "") {
801
806
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -846,30 +851,54 @@ var PromptService = class {
846
851
  return typeof value === "string" ? value : JSON.stringify(value, null, 2);
847
852
  });
848
853
  }
849
- buildIngestPrompt(documentChunk, runtimeOverride) {
854
+ hasOntologyPlaceholders(template) {
855
+ return /\{\{\s*ontology(?:Manifest|ModeInstructions)\s*\}\}/.test(template);
856
+ }
857
+ buildSystemPrompt(template, variables, ontologyContext) {
858
+ const shouldHydrate = Object.keys(variables).some(
859
+ (key) => new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`).test(template)
860
+ ) || ontologyContext != null && this.hasOntologyPlaceholders(template);
861
+ const hydrated = shouldHydrate ? this.hydrate(template, { ...variables, ...ontologyContext ?? {} }) : template;
862
+ return this.hasOntologyPlaceholders(template) ? ontologyContext != null ? hydrated : hydrated.replace(/\{\{\s*ontology(?:Manifest|ModeInstructions)\s*\}\}/g, "") : this.appendOntology(hydrated, ontologyContext);
863
+ }
864
+ appendOntology(systemPrompt, ctx) {
865
+ if (!ctx) return systemPrompt;
866
+ return `${systemPrompt}
867
+
868
+ ${ctx.ontologyModeInstructions}`;
869
+ }
870
+ buildIngestPrompt(documentChunk, runtimeOverride, ontologyContext) {
850
871
  const template = runtimeOverride ?? this.globalOverrides?.ingestSystemPrompt ?? INGEST_SYSTEM_PROMPT;
851
- if (/\{\{\s*documentChunk\s*\}\}/.test(template)) {
872
+ const hasDocumentChunk = /\{\{\s*documentChunk\s*\}\}/.test(template);
873
+ if (hasDocumentChunk || this.hasOntologyPlaceholders(template)) {
852
874
  return {
853
- systemPrompt: this.hydrate(template, { documentChunk }),
854
- userPrompt: "Please extract the facts."
875
+ systemPrompt: this.buildSystemPrompt(template, { documentChunk }, ontologyContext),
876
+ userPrompt: hasDocumentChunk ? "Please extract the facts." : `Document Chunk:
877
+ ${documentChunk}`
855
878
  };
856
879
  }
857
880
  return {
858
- systemPrompt: template,
881
+ systemPrompt: this.appendOntology(template, ontologyContext),
859
882
  userPrompt: `Document Chunk:
860
883
  ${documentChunk}`
861
884
  };
862
885
  }
863
- buildLibrarianPrompt(events, currentFacts, runtimeOverride) {
886
+ buildLibrarianPrompt(events, currentFacts, runtimeOverride, ontologyContext) {
864
887
  const template = runtimeOverride ?? this.globalOverrides?.librarianSystemPrompt ?? LIBRARIAN_SYSTEM_PROMPT;
865
- if (/\{\{\s*events\s*\}\}/.test(template) || /\{\{\s*currentFacts\s*\}\}/.test(template)) {
888
+ const hasEvents = /\{\{\s*events\s*\}\}/.test(template);
889
+ const hasCurrentFacts = /\{\{\s*currentFacts\s*\}\}/.test(template);
890
+ if (hasEvents || hasCurrentFacts || this.hasOntologyPlaceholders(template)) {
866
891
  return {
867
- systemPrompt: this.hydrate(template, { events, currentFacts }),
868
- userPrompt: "Please synthesize the context."
892
+ systemPrompt: this.buildSystemPrompt(template, { events, currentFacts }, ontologyContext),
893
+ userPrompt: hasEvents || hasCurrentFacts ? "Please synthesize the context." : `Events:
894
+ ${JSON.stringify(events, null, 2)}
895
+
896
+ Current Facts:
897
+ ${JSON.stringify(currentFacts, null, 2)}`
869
898
  };
870
899
  }
871
900
  return {
872
- systemPrompt: template,
901
+ systemPrompt: this.appendOntology(template, ontologyContext),
873
902
  userPrompt: `Events:
874
903
  ${JSON.stringify(events, null, 2)}
875
904
 
@@ -902,7 +931,7 @@ The following document anchors are provided for contradiction detection only. Do
902
931
 
903
932
  // src/services/IngestionService.ts
904
933
  var IngestionService = class {
905
- constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService) {
934
+ constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
906
935
  this.db = db;
907
936
  this.prefix = prefix;
908
937
  this.options = options;
@@ -910,6 +939,7 @@ var IngestionService = class {
910
939
  this.searchService = searchService;
911
940
  this.jobManager = jobManager;
912
941
  this.embeddingService = embeddingService;
942
+ this.ontologyService = ontologyService;
913
943
  this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
914
944
  }
915
945
  async ingestDocument(entityId, params) {
@@ -934,23 +964,33 @@ var IngestionService = class {
934
964
  if (chunks.length === 0) return { truncated: false, chunks: 0 };
935
965
  const chunkResults = await withConcurrency(
936
966
  chunks.map((chunk) => async () => {
937
- const { systemPrompt, userPrompt } = this.promptService.buildIngestPrompt(chunk, params.promptOverride);
967
+ const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
968
+ const { systemPrompt, userPrompt } = this.promptService.buildIngestPrompt(
969
+ chunk,
970
+ params.promptOverride,
971
+ ontologyContext
972
+ );
938
973
  const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
939
974
  const result = parseJsonResponse(responseText);
940
- return (Array.isArray(result.facts) ? result.facts : []).map(validateFact).filter((f) => f !== null);
975
+ return {
976
+ facts: (Array.isArray(result.facts) ? result.facts : []).map(validateFact).filter((f) => f !== null),
977
+ ontology_updates: result.ontology_updates
978
+ };
941
979
  }),
942
980
  chunkConcurrency
943
981
  );
944
982
  const seen = /* @__PURE__ */ new Set();
945
- const allValidFacts = [];
946
- for (const facts of chunkResults) {
947
- for (const fact of facts) {
948
- const normalized = fact.title.trim().toLowerCase().replace(/\s+/g, " ");
949
- if (!seen.has(normalized)) {
950
- seen.add(normalized);
951
- allValidFacts.push(fact);
983
+ const orderedChunkFacts = [];
984
+ for (const chunkResult of chunkResults) {
985
+ const dedupedFacts = [];
986
+ for (const fact of chunkResult.facts) {
987
+ const normalizedTitle = normalizeTitleKey(fact.title);
988
+ if (!seen.has(normalizedTitle)) {
989
+ seen.add(normalizedTitle);
990
+ dedupedFacts.push(fact);
952
991
  }
953
992
  }
993
+ orderedChunkFacts.push({ facts: dedupedFacts, ontology_updates: chunkResult.ontology_updates });
954
994
  }
955
995
  const now = Date.now();
956
996
  const insertedFacts = [];
@@ -958,26 +998,63 @@ var IngestionService = class {
958
998
  await this.db.withTransactionAsync(async (tx) => {
959
999
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
960
1000
  await this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, null);
961
- for (const fact of allValidFacts) {
962
- const id = generateId("fact_");
963
- const wikiFact = {
964
- id,
965
- entity_id: entityId,
966
- title: fact.title,
967
- body: fact.body,
968
- tags: fact.tags,
969
- confidence: fact.confidence,
970
- source_type: "immutable_document",
971
- source_hash: sourceHash,
972
- source_ref: sourceRef,
973
- created_at: now,
974
- updated_at: now,
975
- last_accessed_at: null,
976
- access_count: 0,
977
- deleted_at: null
978
- };
979
- await this.entryRepo.upsert(wikiFact, tx);
980
- insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1001
+ const titleIndex = /* @__PURE__ */ new Map();
1002
+ const pendingEdges = [];
1003
+ const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx);
1004
+ for (const existing of existingFacts) {
1005
+ titleIndex.set(normalizeTitleKey(existing.title), {
1006
+ id: existing.id,
1007
+ okf_type: existing.okf_type ?? null
1008
+ });
1009
+ }
1010
+ let ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
1011
+ let { mode, manifest } = ontologyState;
1012
+ for (const { facts, ontology_updates } of orderedChunkFacts) {
1013
+ if (mode === "emergent" && ontology_updates && this.ontologyService) {
1014
+ manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
1015
+ ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
1016
+ mode = ontologyState.mode;
1017
+ }
1018
+ for (const fact of facts) {
1019
+ const ontologyFact = fact;
1020
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
1021
+ const id = generateId("fact_");
1022
+ const wikiFact = {
1023
+ id,
1024
+ entity_id: entityId,
1025
+ title: fact.title,
1026
+ body: fact.body,
1027
+ tags: fact.tags,
1028
+ confidence: fact.confidence,
1029
+ source_type: "immutable_document",
1030
+ source_hash: sourceHash,
1031
+ source_ref: sourceRef,
1032
+ created_at: now,
1033
+ updated_at: now,
1034
+ last_accessed_at: null,
1035
+ access_count: 0,
1036
+ deleted_at: null,
1037
+ okf_type: normalized.okf_type
1038
+ };
1039
+ await this.entryRepo.upsert(wikiFact, tx);
1040
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1041
+ titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
1042
+ if (normalized.edges.length > 0) {
1043
+ pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
1044
+ }
1045
+ }
1046
+ }
1047
+ for (const item of pendingEdges) {
1048
+ await this.ontologyService?.resolveAndPersistEdges(
1049
+ entityId,
1050
+ item.sourceId,
1051
+ item.sourceType,
1052
+ item.edges ?? [],
1053
+ manifest,
1054
+ titleIndex,
1055
+ tx,
1056
+ now
1057
+ );
981
1058
  }
982
1059
  });
983
1060
  await this.searchService.sync(entityId);
@@ -1032,7 +1109,7 @@ function parseEmbedding(blob, text) {
1032
1109
  var FUZZY_THRESHOLD = 0.5;
1033
1110
  var MIN_TOKENS_TO_QUALIFY = 3;
1034
1111
  var MaintenanceService = class {
1035
- constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService) {
1112
+ constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
1036
1113
  this.db = db;
1037
1114
  this.prefix = prefix;
1038
1115
  this.options = options;
@@ -1043,6 +1120,7 @@ var MaintenanceService = class {
1043
1120
  this.searchService = searchService;
1044
1121
  this.jobManager = jobManager;
1045
1122
  this.embeddingService = embeddingService;
1123
+ this.ontologyService = ontologyService;
1046
1124
  this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
1047
1125
  }
1048
1126
  async runPrune(entityId, options) {
@@ -1265,21 +1343,36 @@ var MaintenanceService = class {
1265
1343
  tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
1266
1344
  };
1267
1345
  });
1346
+ const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
1268
1347
  const { systemPrompt, userPrompt } = this.promptService.buildLibrarianPrompt(
1269
1348
  events.reverse(),
1270
1349
  currentFacts,
1271
- promptOverride
1350
+ promptOverride,
1351
+ ontologyContext
1272
1352
  );
1273
1353
  const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
1274
1354
  const result = parseJsonResponse(responseText);
1275
1355
  const facts = Array.isArray(result.facts) ? result.facts : [];
1276
1356
  const tasks = Array.isArray(result.tasks) ? result.tasks : [];
1357
+ const ontologyUpdates = result.ontology_updates;
1277
1358
  const validFacts = facts.map(validateFact).filter((f) => f !== null);
1278
1359
  const validTasks = tasks.map(validateTask).filter((t) => t !== null);
1279
1360
  const now = Date.now();
1280
1361
  const insertedFacts = [];
1281
1362
  await this.db.withTransactionAsync(async (tx) => {
1363
+ let { mode, manifest } = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
1364
+ if (mode === "emergent" && ontologyUpdates && this.ontologyService) {
1365
+ manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontologyUpdates, tx);
1366
+ }
1367
+ const titleIndex = /* @__PURE__ */ new Map();
1368
+ for (const existing of currentFactsRows) {
1369
+ titleIndex.set(normalizeTitleKey(existing.title), {
1370
+ id: existing.id,
1371
+ okf_type: existing.okf_type ?? null
1372
+ });
1373
+ }
1282
1374
  const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
1375
+ const pendingEdges = [];
1283
1376
  for (const fact of validFacts) {
1284
1377
  const newTokens = titleTokens(fact.title);
1285
1378
  let skip = false;
@@ -1296,6 +1389,8 @@ var MaintenanceService = class {
1296
1389
  }
1297
1390
  }
1298
1391
  if (skip) continue;
1392
+ const ontologyFact = fact;
1393
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
1299
1394
  const id = generateId("fact_");
1300
1395
  const factObj = {
1301
1396
  id,
@@ -1311,11 +1406,28 @@ var MaintenanceService = class {
1311
1406
  updated_at: now,
1312
1407
  last_accessed_at: null,
1313
1408
  access_count: 0,
1314
- deleted_at: null
1409
+ deleted_at: null,
1410
+ okf_type: normalized.okf_type
1315
1411
  };
1316
1412
  await this.entryRepo.upsert(factObj, tx);
1317
1413
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1318
1414
  factsForDedupe.push(factObj);
1415
+ titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
1416
+ if (normalized.edges.length > 0) {
1417
+ pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
1418
+ }
1419
+ }
1420
+ for (const item of pendingEdges) {
1421
+ await this.ontologyService?.resolveAndPersistEdges(
1422
+ entityId,
1423
+ item.sourceId,
1424
+ item.sourceType,
1425
+ item.edges ?? [],
1426
+ manifest,
1427
+ titleIndex,
1428
+ tx,
1429
+ now
1430
+ );
1319
1431
  }
1320
1432
  for (const task of validTasks) {
1321
1433
  const id = generateId("task_");