@equationalapplications/core-llm-wiki 4.6.0 → 4.6.1
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/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +182 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +182 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -139,6 +139,157 @@ var PrunePartialFailureError = class extends Error {
|
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
// src/repositories/BaseRepository.ts
|
|
143
|
+
var BaseRepository = class {
|
|
144
|
+
constructor(db, prefix) {
|
|
145
|
+
this.db = db;
|
|
146
|
+
this.prefix = prefix;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Return the DB executor for a given transaction handle.
|
|
150
|
+
* If tx is provided, use it; otherwise fall back to this.db.
|
|
151
|
+
*/
|
|
152
|
+
getExecutor(tx) {
|
|
153
|
+
return tx ?? this.db;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/repositories/EntryRepository.ts
|
|
158
|
+
function mapRowToFact(row) {
|
|
159
|
+
const tags = (() => {
|
|
160
|
+
if (Array.isArray(row.tags)) return row.tags;
|
|
161
|
+
try {
|
|
162
|
+
const p = JSON.parse(row.tags);
|
|
163
|
+
if (Array.isArray(p)) return p;
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
})();
|
|
168
|
+
return {
|
|
169
|
+
id: row.id,
|
|
170
|
+
entity_id: row.entity_id,
|
|
171
|
+
title: row.title,
|
|
172
|
+
body: row.body,
|
|
173
|
+
tags,
|
|
174
|
+
confidence: row.confidence,
|
|
175
|
+
source_type: row.source_type,
|
|
176
|
+
source_hash: row.source_hash ?? null,
|
|
177
|
+
source_ref: row.source_ref ?? null,
|
|
178
|
+
created_at: Number(row.created_at),
|
|
179
|
+
updated_at: Number(row.updated_at),
|
|
180
|
+
last_accessed_at: row.last_accessed_at === null || row.last_accessed_at === void 0 ? null : Number(row.last_accessed_at),
|
|
181
|
+
deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
|
|
182
|
+
access_count: Number(row.access_count ?? 0)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var EntryRepository = class extends BaseRepository {
|
|
186
|
+
constructor() {
|
|
187
|
+
super(...arguments);
|
|
188
|
+
this.chunkSize = 500;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Fetch facts by IDs, optionally scoped to entity IDs.
|
|
192
|
+
* Returns facts in the order of the input IDs (first match wins).
|
|
193
|
+
*/
|
|
194
|
+
async findByIds(ids, scopedEntityIds, tx) {
|
|
195
|
+
const executor = this.getExecutor(tx);
|
|
196
|
+
const rows = [];
|
|
197
|
+
const entityClause = scopedEntityIds && scopedEntityIds.length > 0 ? ` AND entity_id IN (${scopedEntityIds.map(() => "?").join(",")})` : "";
|
|
198
|
+
const entityParams = scopedEntityIds ?? [];
|
|
199
|
+
for (let i = 0; i < ids.length; i += this.chunkSize) {
|
|
200
|
+
const chunk = ids.slice(i, i + this.chunkSize);
|
|
201
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
202
|
+
const chunkRows = await executor.getAllAsync(
|
|
203
|
+
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders})${entityClause} AND deleted_at IS NULL`,
|
|
204
|
+
[...chunk, ...entityParams]
|
|
205
|
+
);
|
|
206
|
+
rows.push(...chunkRows);
|
|
207
|
+
}
|
|
208
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
209
|
+
return ids.map((id) => byId.get(id)).filter((r) => r !== void 0).map(mapRowToFact);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Upsert a WikiFact. Nullable fields set to null when fact value is null.
|
|
213
|
+
* Returns { changes, lastInsertRowId }.
|
|
214
|
+
*/
|
|
215
|
+
async upsert(fact, tx) {
|
|
216
|
+
const executor = this.getExecutor(tx);
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const tagsJson = JSON.stringify(fact.tags);
|
|
219
|
+
const embeddingBlob = fact.embedding_blob instanceof Uint8Array ? fact.embedding_blob : fact.embedding_blob && typeof fact.embedding_blob === "object" && "type" in fact.embedding_blob ? new Uint8Array(fact.embedding_blob.data) : fact.embedding_blob && typeof fact.embedding_blob === "object" ? (() => {
|
|
220
|
+
const obj = fact.embedding_blob;
|
|
221
|
+
const keys = Object.keys(obj).map(Number).sort((a, b) => a - b);
|
|
222
|
+
const arr = new Uint8Array(keys.length);
|
|
223
|
+
for (let i = 0; i < keys.length; i++) arr[i] = obj[String(keys[i])];
|
|
224
|
+
return arr;
|
|
225
|
+
})() : void 0;
|
|
226
|
+
return executor.runAsync(
|
|
227
|
+
`INSERT INTO ${this.prefix}entries (
|
|
228
|
+
id, entity_id, title, body, tags, confidence, source_type,
|
|
229
|
+
source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
|
|
230
|
+
deleted_at, embedding_blob, embedding
|
|
231
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
232
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
233
|
+
entity_id = excluded.entity_id,
|
|
234
|
+
title = excluded.title,
|
|
235
|
+
body = excluded.body,
|
|
236
|
+
tags = excluded.tags,
|
|
237
|
+
confidence = excluded.confidence,
|
|
238
|
+
source_type = excluded.source_type,
|
|
239
|
+
source_hash = excluded.source_hash,
|
|
240
|
+
source_ref = excluded.source_ref,
|
|
241
|
+
updated_at = excluded.updated_at,
|
|
242
|
+
last_accessed_at = excluded.last_accessed_at,
|
|
243
|
+
access_count = excluded.access_count,
|
|
244
|
+
deleted_at = excluded.deleted_at,
|
|
245
|
+
embedding_blob = CASE WHEN excluded.embedding_blob IS NULL THEN embedding_blob ELSE excluded.embedding_blob END,
|
|
246
|
+
embedding = NULL`,
|
|
247
|
+
[
|
|
248
|
+
fact.id,
|
|
249
|
+
fact.entity_id,
|
|
250
|
+
fact.title,
|
|
251
|
+
fact.body,
|
|
252
|
+
tagsJson,
|
|
253
|
+
fact.confidence,
|
|
254
|
+
fact.source_type,
|
|
255
|
+
fact.source_hash,
|
|
256
|
+
fact.source_ref,
|
|
257
|
+
fact.created_at,
|
|
258
|
+
now,
|
|
259
|
+
// updated_at set by repo
|
|
260
|
+
fact.last_accessed_at === null ? null : fact.last_accessed_at,
|
|
261
|
+
fact.access_count,
|
|
262
|
+
fact.deleted_at ?? null,
|
|
263
|
+
embeddingBlob ?? null,
|
|
264
|
+
null
|
|
265
|
+
]
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Soft-delete a single entry by ID scoped to entityId. Sets deleted_at + updated_at.
|
|
270
|
+
*/
|
|
271
|
+
async softDelete(entryId, entityId, tx) {
|
|
272
|
+
const executor = this.getExecutor(tx);
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
return executor.runAsync(
|
|
275
|
+
`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
276
|
+
[now, now, entryId, entityId]
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Fetch IDs + entity_ids of soft-deleted rows older than cutoff for a given entity.
|
|
281
|
+
* Used by runPrune().
|
|
282
|
+
*/
|
|
283
|
+
async getPrunableMetadata(entityId, cutoff, tx) {
|
|
284
|
+
const executor = this.getExecutor(tx);
|
|
285
|
+
return executor.getAllAsync(
|
|
286
|
+
`SELECT id, entity_id FROM ${this.prefix}entries
|
|
287
|
+
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
288
|
+
[entityId, cutoff]
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
142
293
|
// src/prompts.ts
|
|
143
294
|
var LIBRARIAN_SYSTEM_PROMPT = `You are a knowledge extraction agent. Your job is to analyze recent episodic events and extract stable facts and actionable tasks about the user or entity.
|
|
144
295
|
Return ONLY a valid JSON object matching this schema:
|
|
@@ -461,6 +612,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
461
612
|
this.db = db;
|
|
462
613
|
this.options = options;
|
|
463
614
|
this.prefix = options.config?.tablePrefix || "llm_wiki_";
|
|
615
|
+
this.entryRepo = new EntryRepository(db, this.prefix);
|
|
464
616
|
}
|
|
465
617
|
normalizeMiniSearchRow(row) {
|
|
466
618
|
return {
|
|
@@ -916,11 +1068,7 @@ After running the migration SQL, restart your application.`
|
|
|
916
1068
|
let deletedEvents = 0;
|
|
917
1069
|
if (retainSoftDeletedFor !== null) {
|
|
918
1070
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
919
|
-
const entriesToDelete = await this.
|
|
920
|
-
`SELECT id, entity_id FROM ${this.prefix}entries
|
|
921
|
-
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
922
|
-
[entityId, cutoff]
|
|
923
|
-
);
|
|
1071
|
+
const entriesToDelete = await this.entryRepo.getPrunableMetadata(entityId, cutoff);
|
|
924
1072
|
const succeeded = [];
|
|
925
1073
|
let failure = null;
|
|
926
1074
|
for (const row of entriesToDelete) {
|
|
@@ -1476,7 +1624,15 @@ After running the migration SQL, restart your application.`
|
|
|
1476
1624
|
const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
|
|
1477
1625
|
return {
|
|
1478
1626
|
...rest,
|
|
1479
|
-
tags:
|
|
1627
|
+
tags: (() => {
|
|
1628
|
+
if (Array.isArray(rest.tags)) return rest.tags;
|
|
1629
|
+
try {
|
|
1630
|
+
const p = JSON.parse(rest.tags);
|
|
1631
|
+
return Array.isArray(p) ? p : [];
|
|
1632
|
+
} catch {
|
|
1633
|
+
return [];
|
|
1634
|
+
}
|
|
1635
|
+
})()
|
|
1480
1636
|
};
|
|
1481
1637
|
});
|
|
1482
1638
|
}
|
|
@@ -1556,28 +1712,8 @@ After running the migration SQL, restart your application.`
|
|
|
1556
1712
|
* Hydrate full facts by ID. Pass scopedEntityIds to restrict to requested namespaces in SQL
|
|
1557
1713
|
* (defense-in-depth against a rogue VectorRanker returning cross-entity IDs).
|
|
1558
1714
|
*/
|
|
1559
|
-
async _hydrateFactsByIds(ids, scopedEntityIds) {
|
|
1560
|
-
|
|
1561
|
-
const chunkSize = 500;
|
|
1562
|
-
const entityClause = scopedEntityIds && scopedEntityIds.length > 0 ? ` AND entity_id IN (${scopedEntityIds.map(() => "?").join(",")})` : "";
|
|
1563
|
-
const entityParams = scopedEntityIds && scopedEntityIds.length > 0 ? [...scopedEntityIds] : [];
|
|
1564
|
-
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
1565
|
-
const idChunk = ids.slice(i, i + chunkSize);
|
|
1566
|
-
const placeholders = idChunk.map(() => "?").join(",");
|
|
1567
|
-
const chunkRows = await this.db.getAllAsync(
|
|
1568
|
-
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders})${entityClause} AND deleted_at IS NULL`,
|
|
1569
|
-
[...idChunk, ...entityParams]
|
|
1570
|
-
);
|
|
1571
|
-
fullRows.push(...chunkRows);
|
|
1572
|
-
}
|
|
1573
|
-
const byId = new Map(fullRows.map((row) => [row.id, row]));
|
|
1574
|
-
return ids.map((id) => byId.get(id)).filter((fact) => fact !== void 0).map((fact) => {
|
|
1575
|
-
const { embedding: _embedding, embedding_blob: _blob, ...rest } = fact;
|
|
1576
|
-
return {
|
|
1577
|
-
...rest,
|
|
1578
|
-
tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
|
|
1579
|
-
};
|
|
1580
|
-
});
|
|
1715
|
+
async _hydrateFactsByIds(ids, scopedEntityIds, tx) {
|
|
1716
|
+
return this.entryRepo.findByIds(ids, scopedEntityIds, tx);
|
|
1581
1717
|
}
|
|
1582
1718
|
/**
|
|
1583
1719
|
* Strip potentially sensitive data from ranker errors before exposing to host callbacks.
|
|
@@ -2590,7 +2726,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2590
2726
|
const entriesToDelete = await this.db.getAllAsync(q, args);
|
|
2591
2727
|
deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
|
|
2592
2728
|
}
|
|
2593
|
-
const entryPromise = params.entryId ? this.
|
|
2729
|
+
const entryPromise = params.entryId ? this.entryRepo.softDelete(params.entryId, entityId) : null;
|
|
2594
2730
|
const taskPromise = params.taskId ? this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`, [now, now, params.taskId, entityId]) : null;
|
|
2595
2731
|
let refPromise = null;
|
|
2596
2732
|
if (sourceRef || sourceHash) {
|
|
@@ -2727,11 +2863,23 @@ ${chunk}`;
|
|
|
2727
2863
|
);
|
|
2728
2864
|
for (const fact of allValidFacts) {
|
|
2729
2865
|
const id = generateId("fact_");
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2866
|
+
const wikiFact = {
|
|
2867
|
+
id,
|
|
2868
|
+
entity_id: entityId,
|
|
2869
|
+
title: fact.title,
|
|
2870
|
+
body: fact.body,
|
|
2871
|
+
tags: fact.tags,
|
|
2872
|
+
confidence: fact.confidence,
|
|
2873
|
+
source_type: "immutable_document",
|
|
2874
|
+
source_hash: sourceHash,
|
|
2875
|
+
source_ref: sourceRef,
|
|
2876
|
+
created_at: now,
|
|
2877
|
+
updated_at: now,
|
|
2878
|
+
last_accessed_at: null,
|
|
2879
|
+
access_count: 0,
|
|
2880
|
+
deleted_at: null
|
|
2881
|
+
};
|
|
2882
|
+
await this.entryRepo.upsert(wikiFact);
|
|
2735
2883
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2736
2884
|
}
|
|
2737
2885
|
});
|