@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/README.md +96 -0
- package/dist/{chunk-J4GBC6CP.mjs → chunk-2BGLPRT3.mjs} +238 -46
- package/dist/chunk-2BGLPRT3.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +444 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +211 -8
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-NH1_Aigh.d.mts → testing-BMsplvLy.d.mts} +129 -16
- package/dist/{testing-NH1_Aigh.d.ts → testing-BMsplvLy.d.ts} +129 -16
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +155 -43
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-J4GBC6CP.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ Platform-agnostic TypeScript engine for hybrid LLM memory. Features episodic fac
|
|
|
21
21
|
- **Full-featured memory** — Facts, tasks, events, maintenance jobs (librarian, heal, reembed, prune)
|
|
22
22
|
- **Type-safe** — Built with TypeScript, full type exports
|
|
23
23
|
- **Interoperability:** Supports [Open Knowledge Format (OKF) v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) import and export.
|
|
24
|
+
- **Per-entity seeded ontology** — Optional Strict, Emergent, or Off modes govern LLM graph extraction; seed taxonomies per entity and persist typed facts with inline edges.
|
|
24
25
|
|
|
25
26
|
## Installation
|
|
26
27
|
|
|
@@ -445,6 +446,101 @@ Notes:
|
|
|
445
446
|
- A throwing callback is caught (logged via `console.error`) and does not block other subscribers or the underlying job.
|
|
446
447
|
- Subscriptions are scoped to a single `entityId`. There is no wildcard or "all entities" form.
|
|
447
448
|
|
|
449
|
+
## Per-Entity Seeded Ontology
|
|
450
|
+
|
|
451
|
+
Control how librarian and ingest passes classify facts and extract graph relationships. The system defaults to **`off`** so existing deployments behave unchanged.
|
|
452
|
+
|
|
453
|
+
### The Three Modes
|
|
454
|
+
|
|
455
|
+
| Mode | Behavior |
|
|
456
|
+
|------|----------|
|
|
457
|
+
| **`off`** (default) | No ontology guidance. LLM output and persistence match pre-ontology behavior: `okf_type` stays `null` on LLM-created facts; maintenance passes do not create edges. OKF import still populates `okf_type` and edges independently. |
|
|
458
|
+
| **`strict`** | The LLM must use only `node_types` and `edge_types` from the entity manifest. Invalid `okf_type` falls back to an untyped fact with no edges; invalid individual edges are dropped while a valid `okf_type` and matching edges are kept. |
|
|
459
|
+
| **`emergent`** | Same validation as Strict, plus the LLM may return `ontology_updates` with new node/edge types. Updates are append-only (deduped by `type` string) and take effect before facts from the same response are validated. |
|
|
460
|
+
|
|
461
|
+
Mode resolution per entity: persisted DB row `mode` (when present) → `seedManifests[entityId].mode` (when no row but a seed exists) → `WikiConfig.ontology.mode` → `'off'`.
|
|
462
|
+
|
|
463
|
+
### WikiConfig
|
|
464
|
+
|
|
465
|
+
Set a global default mode and bootstrap manifests for known entities at construction time:
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
const wikiMemory = new WikiMemory(db, {
|
|
469
|
+
llmProvider,
|
|
470
|
+
config: {
|
|
471
|
+
ontology: {
|
|
472
|
+
mode: 'strict', // global default when an entity has no per-entity override
|
|
473
|
+
seedManifests: {
|
|
474
|
+
'team-alpha': {
|
|
475
|
+
mode: 'emergent', // optional per-entity override
|
|
476
|
+
manifest: {
|
|
477
|
+
node_types: [
|
|
478
|
+
{ type: 'person', description: 'An individual or user.' },
|
|
479
|
+
{ type: 'project', description: 'An ongoing initiative.' },
|
|
480
|
+
],
|
|
481
|
+
edge_types: [
|
|
482
|
+
{
|
|
483
|
+
type: 'contributes_to',
|
|
484
|
+
source_type: 'person',
|
|
485
|
+
target_type: 'project',
|
|
486
|
+
description: 'Person working on a project.',
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`seedManifests` entries are written to SQLite on first access when no row exists for that entity.
|
|
498
|
+
|
|
499
|
+
### Public API
|
|
500
|
+
|
|
501
|
+
Read or seed an entity's ontology at runtime:
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// Read effective mode + manifest (DB row, then seedManifests fallback)
|
|
505
|
+
const ontology = await wikiMemory.getOntologyManifest('team-alpha');
|
|
506
|
+
// { mode: 'emergent', manifest: { node_types: [...], edge_types: [...] } }
|
|
507
|
+
// null when no row and no seed entry
|
|
508
|
+
|
|
509
|
+
// Seed or replace manifest; optional per-entity mode override
|
|
510
|
+
await wikiMemory.setOntologyManifest('team-alpha', {
|
|
511
|
+
node_types: [{ type: 'person', description: 'An individual.' }],
|
|
512
|
+
edge_types: [{
|
|
513
|
+
type: 'reports_to',
|
|
514
|
+
source_type: 'person',
|
|
515
|
+
target_type: 'person',
|
|
516
|
+
description: 'Reporting hierarchy.',
|
|
517
|
+
}],
|
|
518
|
+
}, { mode: 'strict' });
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Fact Shape Extensions
|
|
522
|
+
|
|
523
|
+
In **Strict** and **Emergent** modes, librarian and ingest JSON may include typed facts with inline edges:
|
|
524
|
+
|
|
525
|
+
```json
|
|
526
|
+
{
|
|
527
|
+
"facts": [{
|
|
528
|
+
"title": "Jane reports to Bob",
|
|
529
|
+
"body": "Jane reports to Bob Smith.",
|
|
530
|
+
"tags": [],
|
|
531
|
+
"confidence": "certain",
|
|
532
|
+
"okf_type": "person",
|
|
533
|
+
"edges": [{ "edge_type": "reports_to", "target_title": "Bob Smith" }]
|
|
534
|
+
}]
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
- `okf_type` maps to a `node_types[].type` entry (case-insensitive lookup; canonical manifest casing is persisted).
|
|
539
|
+
- `edges` are resolved by `target_title` within the same maintenance transaction and persisted via `EdgeRepository`.
|
|
540
|
+
- Invalid `okf_type` falls back to `null` with no edges for that fact. Invalid individual edges are dropped; valid `okf_type` and matching edges are still persisted.
|
|
541
|
+
|
|
542
|
+
See the design spec: [`docs/superpowers/specs/2026-06-23-per-entity-seeded-ontology-design.md`](https://github.com/equationalapplications/expo-llm-wiki/blob/main/docs/superpowers/specs/2026-06-23-per-entity-seeded-ontology-design.md).
|
|
543
|
+
|
|
448
544
|
## OKF Import/Export
|
|
449
545
|
|
|
450
546
|
The core package integrates with `@equationalapplications/core-okf` to seamlessly adapt wiki data dumps to and from Open Knowledge Format (OKF) v0.1 bundles.
|
|
@@ -606,30 +606,54 @@ var PromptService = class {
|
|
|
606
606
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
607
607
|
});
|
|
608
608
|
}
|
|
609
|
-
|
|
609
|
+
hasOntologyPlaceholders(template) {
|
|
610
|
+
return /\{\{\s*ontology(?:Manifest|ModeInstructions)\s*\}\}/.test(template);
|
|
611
|
+
}
|
|
612
|
+
buildSystemPrompt(template, variables, ontologyContext) {
|
|
613
|
+
const shouldHydrate = Object.keys(variables).some(
|
|
614
|
+
(key) => new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`).test(template)
|
|
615
|
+
) || ontologyContext != null && this.hasOntologyPlaceholders(template);
|
|
616
|
+
const hydrated = shouldHydrate ? this.hydrate(template, { ...variables, ...ontologyContext ?? {} }) : template;
|
|
617
|
+
return this.hasOntologyPlaceholders(template) ? ontologyContext != null ? hydrated : hydrated.replace(/\{\{\s*ontology(?:Manifest|ModeInstructions)\s*\}\}/g, "") : this.appendOntology(hydrated, ontologyContext);
|
|
618
|
+
}
|
|
619
|
+
appendOntology(systemPrompt, ctx) {
|
|
620
|
+
if (!ctx) return systemPrompt;
|
|
621
|
+
return `${systemPrompt}
|
|
622
|
+
|
|
623
|
+
${ctx.ontologyModeInstructions}`;
|
|
624
|
+
}
|
|
625
|
+
buildIngestPrompt(documentChunk, runtimeOverride, ontologyContext) {
|
|
610
626
|
const template = runtimeOverride ?? this.globalOverrides?.ingestSystemPrompt ?? INGEST_SYSTEM_PROMPT;
|
|
611
|
-
|
|
627
|
+
const hasDocumentChunk = /\{\{\s*documentChunk\s*\}\}/.test(template);
|
|
628
|
+
if (hasDocumentChunk || this.hasOntologyPlaceholders(template)) {
|
|
612
629
|
return {
|
|
613
|
-
systemPrompt: this.
|
|
614
|
-
userPrompt: "Please extract the facts."
|
|
630
|
+
systemPrompt: this.buildSystemPrompt(template, { documentChunk }, ontologyContext),
|
|
631
|
+
userPrompt: hasDocumentChunk ? "Please extract the facts." : `Document Chunk:
|
|
632
|
+
${documentChunk}`
|
|
615
633
|
};
|
|
616
634
|
}
|
|
617
635
|
return {
|
|
618
|
-
systemPrompt: template,
|
|
636
|
+
systemPrompt: this.appendOntology(template, ontologyContext),
|
|
619
637
|
userPrompt: `Document Chunk:
|
|
620
638
|
${documentChunk}`
|
|
621
639
|
};
|
|
622
640
|
}
|
|
623
|
-
buildLibrarianPrompt(events, currentFacts, runtimeOverride) {
|
|
641
|
+
buildLibrarianPrompt(events, currentFacts, runtimeOverride, ontologyContext) {
|
|
624
642
|
const template = runtimeOverride ?? this.globalOverrides?.librarianSystemPrompt ?? LIBRARIAN_SYSTEM_PROMPT;
|
|
625
|
-
|
|
643
|
+
const hasEvents = /\{\{\s*events\s*\}\}/.test(template);
|
|
644
|
+
const hasCurrentFacts = /\{\{\s*currentFacts\s*\}\}/.test(template);
|
|
645
|
+
if (hasEvents || hasCurrentFacts || this.hasOntologyPlaceholders(template)) {
|
|
626
646
|
return {
|
|
627
|
-
systemPrompt: this.
|
|
628
|
-
userPrompt: "Please synthesize the context."
|
|
647
|
+
systemPrompt: this.buildSystemPrompt(template, { events, currentFacts }, ontologyContext),
|
|
648
|
+
userPrompt: hasEvents || hasCurrentFacts ? "Please synthesize the context." : `Events:
|
|
649
|
+
${JSON.stringify(events, null, 2)}
|
|
650
|
+
|
|
651
|
+
Current Facts:
|
|
652
|
+
${JSON.stringify(currentFacts, null, 2)}`
|
|
629
653
|
};
|
|
630
654
|
}
|
|
631
655
|
return {
|
|
632
|
-
systemPrompt: template,
|
|
656
|
+
systemPrompt: this.appendOntology(template, ontologyContext),
|
|
633
657
|
userPrompt: `Events:
|
|
634
658
|
${JSON.stringify(events, null, 2)}
|
|
635
659
|
|
|
@@ -875,6 +899,91 @@ function jaccardScore(a, b) {
|
|
|
875
899
|
return intersection.size / union.size;
|
|
876
900
|
}
|
|
877
901
|
|
|
902
|
+
// src/utils/ontology.ts
|
|
903
|
+
function emptyManifest() {
|
|
904
|
+
return { node_types: [], edge_types: [] };
|
|
905
|
+
}
|
|
906
|
+
function normalizeTitleKey(title) {
|
|
907
|
+
return title.trim().toLowerCase().replace(/\s+/g, " ");
|
|
908
|
+
}
|
|
909
|
+
function resolveNodeType(raw, manifest) {
|
|
910
|
+
const slug = raw.trim();
|
|
911
|
+
if (!slug) return null;
|
|
912
|
+
const hit = manifest.node_types.find((n) => n.type.toLowerCase() === slug.toLowerCase());
|
|
913
|
+
return hit?.type ?? null;
|
|
914
|
+
}
|
|
915
|
+
function resolveEdgeDefinition(rawEdgeType, manifest) {
|
|
916
|
+
const slug = rawEdgeType.trim();
|
|
917
|
+
if (!slug) return null;
|
|
918
|
+
return manifest.edge_types.find((e) => e.type.toLowerCase() === slug.toLowerCase()) ?? null;
|
|
919
|
+
}
|
|
920
|
+
function validateManifest(manifest) {
|
|
921
|
+
const nodeSlugs = /* @__PURE__ */ new Set();
|
|
922
|
+
for (const node of manifest.node_types ?? []) {
|
|
923
|
+
const type = node.type?.trim();
|
|
924
|
+
if (!type) throw new Error("Ontology node type slug must be non-empty");
|
|
925
|
+
const key = type.toLowerCase();
|
|
926
|
+
if (nodeSlugs.has(key)) throw new Error(`Duplicate node type: ${type}`);
|
|
927
|
+
nodeSlugs.add(key);
|
|
928
|
+
}
|
|
929
|
+
const edgeSlugs = /* @__PURE__ */ new Set();
|
|
930
|
+
for (const edge of manifest.edge_types ?? []) {
|
|
931
|
+
const edgeType = edge.type?.trim();
|
|
932
|
+
const sourceType = edge.source_type?.trim();
|
|
933
|
+
const targetType = edge.target_type?.trim();
|
|
934
|
+
if (!edgeType) throw new Error("Ontology edge type slug must be non-empty");
|
|
935
|
+
const edgeKey = edgeType.toLowerCase();
|
|
936
|
+
if (edgeSlugs.has(edgeKey)) throw new Error(`Duplicate edge type: ${edgeType}`);
|
|
937
|
+
edgeSlugs.add(edgeKey);
|
|
938
|
+
if (!sourceType || !targetType || !nodeSlugs.has(sourceType.toLowerCase()) || !nodeSlugs.has(targetType.toLowerCase())) {
|
|
939
|
+
throw new Error(`Edge type ${edgeType} references unknown node type`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function mergeOntologyUpdates(current, updates) {
|
|
944
|
+
const node_types = [...current.node_types];
|
|
945
|
+
const edge_types = [...current.edge_types];
|
|
946
|
+
const nodeSlugs = new Set(node_types.map((n) => n.type.trim().toLowerCase()));
|
|
947
|
+
const edgeSlugs = new Set(edge_types.map((e) => e.type.trim().toLowerCase()));
|
|
948
|
+
for (const node of updates.node_types ?? []) {
|
|
949
|
+
const type = node?.type?.trim();
|
|
950
|
+
if (!type) continue;
|
|
951
|
+
const key = type.toLowerCase();
|
|
952
|
+
if (nodeSlugs.has(key)) continue;
|
|
953
|
+
node_types.push({ type, description: String(node.description ?? "") });
|
|
954
|
+
nodeSlugs.add(key);
|
|
955
|
+
}
|
|
956
|
+
for (const edge of updates.edge_types ?? []) {
|
|
957
|
+
const edgeType = edge?.type?.trim();
|
|
958
|
+
const sourceType = edge?.source_type?.trim();
|
|
959
|
+
const targetType = edge?.target_type?.trim();
|
|
960
|
+
if (!edgeType || !sourceType || !targetType) continue;
|
|
961
|
+
const edgeKey = edgeType.toLowerCase();
|
|
962
|
+
if (edgeSlugs.has(edgeKey)) continue;
|
|
963
|
+
if (!nodeSlugs.has(sourceType.toLowerCase()) || !nodeSlugs.has(targetType.toLowerCase())) continue;
|
|
964
|
+
edge_types.push({
|
|
965
|
+
type: edgeType,
|
|
966
|
+
source_type: sourceType,
|
|
967
|
+
target_type: targetType,
|
|
968
|
+
description: String(edge.description ?? "")
|
|
969
|
+
});
|
|
970
|
+
edgeSlugs.add(edgeKey);
|
|
971
|
+
}
|
|
972
|
+
return { node_types, edge_types };
|
|
973
|
+
}
|
|
974
|
+
function validateInlineEdges(sourceType, _targetType, edges, manifest) {
|
|
975
|
+
if (!Array.isArray(edges)) return [];
|
|
976
|
+
const valid = [];
|
|
977
|
+
for (const edge of edges) {
|
|
978
|
+
if (typeof edge?.edge_type !== "string" || typeof edge?.target_title !== "string") continue;
|
|
979
|
+
const def = resolveEdgeDefinition(edge.edge_type, manifest);
|
|
980
|
+
if (!def) continue;
|
|
981
|
+
if (def.source_type.toLowerCase() !== sourceType.toLowerCase()) continue;
|
|
982
|
+
valid.push({ edge_type: def.type, target_title: edge.target_title });
|
|
983
|
+
}
|
|
984
|
+
return valid;
|
|
985
|
+
}
|
|
986
|
+
|
|
878
987
|
// src/utils/ids.ts
|
|
879
988
|
function generateId(prefix = "") {
|
|
880
989
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
@@ -892,7 +1001,7 @@ function generateId(prefix = "") {
|
|
|
892
1001
|
|
|
893
1002
|
// src/services/IngestionService.ts
|
|
894
1003
|
var IngestionService = class {
|
|
895
|
-
constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService) {
|
|
1004
|
+
constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
896
1005
|
this.db = db;
|
|
897
1006
|
this.prefix = prefix;
|
|
898
1007
|
this.options = options;
|
|
@@ -900,6 +1009,7 @@ var IngestionService = class {
|
|
|
900
1009
|
this.searchService = searchService;
|
|
901
1010
|
this.jobManager = jobManager;
|
|
902
1011
|
this.embeddingService = embeddingService;
|
|
1012
|
+
this.ontologyService = ontologyService;
|
|
903
1013
|
this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
|
|
904
1014
|
}
|
|
905
1015
|
async ingestDocument(entityId, params) {
|
|
@@ -924,23 +1034,33 @@ var IngestionService = class {
|
|
|
924
1034
|
if (chunks.length === 0) return { truncated: false, chunks: 0 };
|
|
925
1035
|
const chunkResults = await withConcurrency(
|
|
926
1036
|
chunks.map((chunk) => async () => {
|
|
927
|
-
const
|
|
1037
|
+
const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
|
|
1038
|
+
const { systemPrompt, userPrompt } = this.promptService.buildIngestPrompt(
|
|
1039
|
+
chunk,
|
|
1040
|
+
params.promptOverride,
|
|
1041
|
+
ontologyContext
|
|
1042
|
+
);
|
|
928
1043
|
const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
|
|
929
1044
|
const result = parseJsonResponse(responseText);
|
|
930
|
-
return
|
|
1045
|
+
return {
|
|
1046
|
+
facts: (Array.isArray(result.facts) ? result.facts : []).map(validateFact).filter((f) => f !== null),
|
|
1047
|
+
ontology_updates: result.ontology_updates
|
|
1048
|
+
};
|
|
931
1049
|
}),
|
|
932
1050
|
chunkConcurrency
|
|
933
1051
|
);
|
|
934
1052
|
const seen = /* @__PURE__ */ new Set();
|
|
935
|
-
const
|
|
936
|
-
for (const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1053
|
+
const orderedChunkFacts = [];
|
|
1054
|
+
for (const chunkResult of chunkResults) {
|
|
1055
|
+
const dedupedFacts = [];
|
|
1056
|
+
for (const fact of chunkResult.facts) {
|
|
1057
|
+
const normalizedTitle = normalizeTitleKey(fact.title);
|
|
1058
|
+
if (!seen.has(normalizedTitle)) {
|
|
1059
|
+
seen.add(normalizedTitle);
|
|
1060
|
+
dedupedFacts.push(fact);
|
|
942
1061
|
}
|
|
943
1062
|
}
|
|
1063
|
+
orderedChunkFacts.push({ facts: dedupedFacts, ontology_updates: chunkResult.ontology_updates });
|
|
944
1064
|
}
|
|
945
1065
|
const now = Date.now();
|
|
946
1066
|
const insertedFacts = [];
|
|
@@ -948,26 +1068,63 @@ var IngestionService = class {
|
|
|
948
1068
|
await this.db.withTransactionAsync(async (tx) => {
|
|
949
1069
|
deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
|
|
950
1070
|
await this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, null);
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1071
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
1072
|
+
const pendingEdges = [];
|
|
1073
|
+
const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx);
|
|
1074
|
+
for (const existing of existingFacts) {
|
|
1075
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
1076
|
+
id: existing.id,
|
|
1077
|
+
okf_type: existing.okf_type ?? null
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
let ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
1081
|
+
let { mode, manifest } = ontologyState;
|
|
1082
|
+
for (const { facts, ontology_updates } of orderedChunkFacts) {
|
|
1083
|
+
if (mode === "emergent" && ontology_updates && this.ontologyService) {
|
|
1084
|
+
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
|
|
1085
|
+
ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
|
|
1086
|
+
mode = ontologyState.mode;
|
|
1087
|
+
}
|
|
1088
|
+
for (const fact of facts) {
|
|
1089
|
+
const ontologyFact = fact;
|
|
1090
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
|
|
1091
|
+
const id = generateId("fact_");
|
|
1092
|
+
const wikiFact = {
|
|
1093
|
+
id,
|
|
1094
|
+
entity_id: entityId,
|
|
1095
|
+
title: fact.title,
|
|
1096
|
+
body: fact.body,
|
|
1097
|
+
tags: fact.tags,
|
|
1098
|
+
confidence: fact.confidence,
|
|
1099
|
+
source_type: "immutable_document",
|
|
1100
|
+
source_hash: sourceHash,
|
|
1101
|
+
source_ref: sourceRef,
|
|
1102
|
+
created_at: now,
|
|
1103
|
+
updated_at: now,
|
|
1104
|
+
last_accessed_at: null,
|
|
1105
|
+
access_count: 0,
|
|
1106
|
+
deleted_at: null,
|
|
1107
|
+
okf_type: normalized.okf_type
|
|
1108
|
+
};
|
|
1109
|
+
await this.entryRepo.upsert(wikiFact, tx);
|
|
1110
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1111
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
1112
|
+
if (normalized.edges.length > 0) {
|
|
1113
|
+
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
for (const item of pendingEdges) {
|
|
1118
|
+
await this.ontologyService?.resolveAndPersistEdges(
|
|
1119
|
+
entityId,
|
|
1120
|
+
item.sourceId,
|
|
1121
|
+
item.sourceType,
|
|
1122
|
+
item.edges ?? [],
|
|
1123
|
+
manifest,
|
|
1124
|
+
titleIndex,
|
|
1125
|
+
tx,
|
|
1126
|
+
now
|
|
1127
|
+
);
|
|
971
1128
|
}
|
|
972
1129
|
});
|
|
973
1130
|
await this.searchService.sync(entityId);
|
|
@@ -994,7 +1151,7 @@ var IngestionService = class {
|
|
|
994
1151
|
var FUZZY_THRESHOLD = 0.5;
|
|
995
1152
|
var MIN_TOKENS_TO_QUALIFY = 3;
|
|
996
1153
|
var MaintenanceService = class {
|
|
997
|
-
constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService) {
|
|
1154
|
+
constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
998
1155
|
this.db = db;
|
|
999
1156
|
this.prefix = prefix;
|
|
1000
1157
|
this.options = options;
|
|
@@ -1005,6 +1162,7 @@ var MaintenanceService = class {
|
|
|
1005
1162
|
this.searchService = searchService;
|
|
1006
1163
|
this.jobManager = jobManager;
|
|
1007
1164
|
this.embeddingService = embeddingService;
|
|
1165
|
+
this.ontologyService = ontologyService;
|
|
1008
1166
|
this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
|
|
1009
1167
|
}
|
|
1010
1168
|
async runPrune(entityId, options) {
|
|
@@ -1227,21 +1385,36 @@ var MaintenanceService = class {
|
|
|
1227
1385
|
tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
|
|
1228
1386
|
};
|
|
1229
1387
|
});
|
|
1388
|
+
const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
|
|
1230
1389
|
const { systemPrompt, userPrompt } = this.promptService.buildLibrarianPrompt(
|
|
1231
1390
|
events.reverse(),
|
|
1232
1391
|
currentFacts,
|
|
1233
|
-
promptOverride
|
|
1392
|
+
promptOverride,
|
|
1393
|
+
ontologyContext
|
|
1234
1394
|
);
|
|
1235
1395
|
const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
|
|
1236
1396
|
const result = parseJsonResponse(responseText);
|
|
1237
1397
|
const facts = Array.isArray(result.facts) ? result.facts : [];
|
|
1238
1398
|
const tasks = Array.isArray(result.tasks) ? result.tasks : [];
|
|
1399
|
+
const ontologyUpdates = result.ontology_updates;
|
|
1239
1400
|
const validFacts = facts.map(validateFact).filter((f) => f !== null);
|
|
1240
1401
|
const validTasks = tasks.map(validateTask).filter((t) => t !== null);
|
|
1241
1402
|
const now = Date.now();
|
|
1242
1403
|
const insertedFacts = [];
|
|
1243
1404
|
await this.db.withTransactionAsync(async (tx) => {
|
|
1405
|
+
let { mode, manifest } = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
1406
|
+
if (mode === "emergent" && ontologyUpdates && this.ontologyService) {
|
|
1407
|
+
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontologyUpdates, tx);
|
|
1408
|
+
}
|
|
1409
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
1410
|
+
for (const existing of currentFactsRows) {
|
|
1411
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
1412
|
+
id: existing.id,
|
|
1413
|
+
okf_type: existing.okf_type ?? null
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1244
1416
|
const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
|
|
1417
|
+
const pendingEdges = [];
|
|
1245
1418
|
for (const fact of validFacts) {
|
|
1246
1419
|
const newTokens = titleTokens(fact.title);
|
|
1247
1420
|
let skip = false;
|
|
@@ -1258,6 +1431,8 @@ var MaintenanceService = class {
|
|
|
1258
1431
|
}
|
|
1259
1432
|
}
|
|
1260
1433
|
if (skip) continue;
|
|
1434
|
+
const ontologyFact = fact;
|
|
1435
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
|
|
1261
1436
|
const id = generateId("fact_");
|
|
1262
1437
|
const factObj = {
|
|
1263
1438
|
id,
|
|
@@ -1273,11 +1448,28 @@ var MaintenanceService = class {
|
|
|
1273
1448
|
updated_at: now,
|
|
1274
1449
|
last_accessed_at: null,
|
|
1275
1450
|
access_count: 0,
|
|
1276
|
-
deleted_at: null
|
|
1451
|
+
deleted_at: null,
|
|
1452
|
+
okf_type: normalized.okf_type
|
|
1277
1453
|
};
|
|
1278
1454
|
await this.entryRepo.upsert(factObj, tx);
|
|
1279
1455
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1280
1456
|
factsForDedupe.push(factObj);
|
|
1457
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
1458
|
+
if (normalized.edges.length > 0) {
|
|
1459
|
+
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
for (const item of pendingEdges) {
|
|
1463
|
+
await this.ontologyService?.resolveAndPersistEdges(
|
|
1464
|
+
entityId,
|
|
1465
|
+
item.sourceId,
|
|
1466
|
+
item.sourceType,
|
|
1467
|
+
item.edges ?? [],
|
|
1468
|
+
manifest,
|
|
1469
|
+
titleIndex,
|
|
1470
|
+
tx,
|
|
1471
|
+
now
|
|
1472
|
+
);
|
|
1281
1473
|
}
|
|
1282
1474
|
for (const task of validTasks) {
|
|
1283
1475
|
const id = generateId("task_");
|
|
@@ -2614,6 +2806,6 @@ var WriteService = class {
|
|
|
2614
2806
|
}
|
|
2615
2807
|
};
|
|
2616
2808
|
|
|
2617
|
-
export { EmbeddingService, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WriteService, __privateAdd, __privateGet, __privateSet, generateId, normalizeSourceHash, normalizeSourceRef, parseEmbedding };
|
|
2618
|
-
//# sourceMappingURL=chunk-
|
|
2619
|
-
//# sourceMappingURL=chunk-
|
|
2809
|
+
export { EmbeddingService, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WriteService, __privateAdd, __privateGet, __privateSet, emptyManifest, generateId, mergeOntologyUpdates, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinition, resolveNodeType, validateInlineEdges, validateManifest };
|
|
2810
|
+
//# sourceMappingURL=chunk-2BGLPRT3.mjs.map
|
|
2811
|
+
//# sourceMappingURL=chunk-2BGLPRT3.mjs.map
|