@dudousxd/nestjs-agent-rag 0.13.1 → 0.14.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/README.md +68 -0
- package/dist/index.cjs +435 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +207 -14
- package/dist/index.d.ts +207 -14
- package/dist/index.js +425 -25
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +174 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +60 -0
- package/dist/nestjs/index.d.ts +60 -0
- package/dist/nestjs/index.js +149 -0
- package/dist/nestjs/index.js.map +1 -0
- package/package.json +24 -4
package/dist/index.cjs
CHANGED
|
@@ -19,8 +19,8 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
19
19
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
20
|
|
|
21
21
|
// src/index.ts
|
|
22
|
-
var
|
|
23
|
-
__export(
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
24
|
DEFAULT_STOP_WORDS: () => DEFAULT_STOP_WORDS,
|
|
25
25
|
EmbeddingRetriever: () => EmbeddingRetriever,
|
|
26
26
|
FilteredRetriever: () => FilteredRetriever,
|
|
@@ -35,8 +35,11 @@ __export(index_exports, {
|
|
|
35
35
|
PgLexicalVectorStore: () => PgLexicalVectorStore,
|
|
36
36
|
PgVectorStore: () => PgVectorStore,
|
|
37
37
|
RAG_DIAGNOSTIC_LIB: () => RAG_DIAGNOSTIC_LIB,
|
|
38
|
+
RAG_ENTITY_KEY: () => RAG_ENTITY_KEY,
|
|
38
39
|
RAG_RETRIEVAL_CHANNEL: () => RAG_RETRIEVAL_CHANNEL,
|
|
39
40
|
RAG_RETRIEVAL_EVENT: () => RAG_RETRIEVAL_EVENT,
|
|
41
|
+
RagIndex: () => RagIndex,
|
|
42
|
+
RagScopeRequiredError: () => RagScopeRequiredError,
|
|
40
43
|
RedisVectorSchemaMismatchError: () => RedisVectorSchemaMismatchError,
|
|
41
44
|
RedisVectorStore: () => RedisVectorStore,
|
|
42
45
|
RerankingRetriever: () => RerankingRetriever,
|
|
@@ -44,6 +47,7 @@ __export(index_exports, {
|
|
|
44
47
|
TreeNavigationRetriever: () => TreeNavigationRetriever,
|
|
45
48
|
TwoStageRetriever: () => TwoStageRetriever,
|
|
46
49
|
UnsafeRemovalError: () => UnsafeRemovalError,
|
|
50
|
+
anyOf: () => anyOf,
|
|
47
51
|
anyTermTsquery: () => anyTermTsquery,
|
|
48
52
|
applyMetadataPatch: () => applyMetadataPatch,
|
|
49
53
|
buildDocumentTree: () => buildDocumentTree,
|
|
@@ -59,6 +63,7 @@ __export(index_exports, {
|
|
|
59
63
|
describesRetrieval: () => describesRetrieval,
|
|
60
64
|
detectHeadings: () => detectHeadings,
|
|
61
65
|
documentIdOf: () => documentIdOf,
|
|
66
|
+
embedCountingUsage: () => embedCountingUsage,
|
|
62
67
|
emitRagRetrieval: () => emitRagRetrieval,
|
|
63
68
|
estimateTokens: () => estimateTokens,
|
|
64
69
|
hasSearchSyntax: () => hasSearchSyntax,
|
|
@@ -74,13 +79,15 @@ __export(index_exports, {
|
|
|
74
79
|
openAiChatTreeLlm: () => openAiChatTreeLlm,
|
|
75
80
|
openAiEmbeddings: () => openAiEmbeddings,
|
|
76
81
|
parseJsonReply: () => parseJsonReply,
|
|
82
|
+
ragFilterableFields: () => ragFilterableFields,
|
|
83
|
+
replaceDocumentChunks: () => replaceDocumentChunks,
|
|
77
84
|
spanText: () => spanText,
|
|
78
85
|
stableHash: () => stableHash,
|
|
79
86
|
toPgClient: () => toPgClient,
|
|
80
87
|
treeLlmFromModelProvider: () => treeLlmFromModelProvider,
|
|
81
88
|
walkTree: () => walkTree
|
|
82
89
|
});
|
|
83
|
-
module.exports = __toCommonJS(
|
|
90
|
+
module.exports = __toCommonJS(src_exports);
|
|
84
91
|
|
|
85
92
|
// src/chunk.ts
|
|
86
93
|
function chunkText(text, options = {}) {
|
|
@@ -158,6 +165,16 @@ function chunkByRecord(text, chunkSize, separator) {
|
|
|
158
165
|
__name(chunkByRecord, "chunkByRecord");
|
|
159
166
|
|
|
160
167
|
// src/vector-store.ts
|
|
168
|
+
async function replaceDocumentChunks(store, documentId, records) {
|
|
169
|
+
const atomic = store;
|
|
170
|
+
if (typeof atomic.replaceDocument === "function") {
|
|
171
|
+
await atomic.replaceDocument(documentId, records);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await store.remove(documentId);
|
|
175
|
+
await store.upsert(records);
|
|
176
|
+
}
|
|
177
|
+
__name(replaceDocumentChunks, "replaceDocumentChunks");
|
|
161
178
|
function isLexicalVectorStore(store) {
|
|
162
179
|
return typeof store.searchText === "function";
|
|
163
180
|
}
|
|
@@ -218,12 +235,19 @@ function applyMetadataPatch(metadata, patch) {
|
|
|
218
235
|
__name(applyMetadataPatch, "applyMetadataPatch");
|
|
219
236
|
|
|
220
237
|
// src/filter.ts
|
|
238
|
+
function isMissingValue(value) {
|
|
239
|
+
return value === void 0 || Array.isArray(value) && value.includes(void 0);
|
|
240
|
+
}
|
|
241
|
+
__name(isMissingValue, "isMissingValue");
|
|
221
242
|
function matchesFilter(metadata, filter) {
|
|
222
243
|
const entries = Object.entries(filter);
|
|
223
244
|
if (metadata === void 0) {
|
|
224
245
|
return entries.length === 0;
|
|
225
246
|
}
|
|
226
247
|
return entries.every(([key, expected]) => {
|
|
248
|
+
if (isMissingValue(expected)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
227
251
|
if (Array.isArray(expected)) {
|
|
228
252
|
const actual = metadata[key];
|
|
229
253
|
return Array.isArray(actual) ? actual.some((value) => expected.includes(value)) : expected.includes(actual);
|
|
@@ -236,9 +260,18 @@ function filterMatchesNothing(filter) {
|
|
|
236
260
|
if (filter === void 0) {
|
|
237
261
|
return false;
|
|
238
262
|
}
|
|
239
|
-
return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);
|
|
263
|
+
return Object.values(filter).some((value) => isMissingValue(value) || Array.isArray(value) && value.length === 0);
|
|
240
264
|
}
|
|
241
265
|
__name(filterMatchesNothing, "filterMatchesNothing");
|
|
266
|
+
function undefinedFilterKeys(filter) {
|
|
267
|
+
return Object.entries(filter).filter(([, value]) => isMissingValue(value)).map(([key]) => key);
|
|
268
|
+
}
|
|
269
|
+
__name(undefinedFilterKeys, "undefinedFilterKeys");
|
|
270
|
+
function undefinedFilterMessage(keys) {
|
|
271
|
+
const named = keys.map((key) => `"${key}"`).join(", ");
|
|
272
|
+
return `removeWhere() refuses a filter with no value for ${named}: the scope was probably never set.`;
|
|
273
|
+
}
|
|
274
|
+
__name(undefinedFilterMessage, "undefinedFilterMessage");
|
|
242
275
|
|
|
243
276
|
// src/memory-vector-store.ts
|
|
244
277
|
var MemoryVectorStore = class {
|
|
@@ -375,6 +408,10 @@ var MemoryVectorStore = class {
|
|
|
375
408
|
if (Object.keys(filter).length === 0) {
|
|
376
409
|
throw new UnsafeRemovalError("empty-filter", "removeWhere() refuses an empty filter: it would delete every chunk in the store. Pass a filter that scopes the removal, or delete deliberately with removeMany(await store.listDocumentIds()).");
|
|
377
410
|
}
|
|
411
|
+
const missing = undefinedFilterKeys(filter);
|
|
412
|
+
if (missing.length > 0) {
|
|
413
|
+
throw new UnsafeRemovalError("undefined-value", undefinedFilterMessage(missing));
|
|
414
|
+
}
|
|
378
415
|
if (filterMatchesNothing(filter)) {
|
|
379
416
|
return 0;
|
|
380
417
|
}
|
|
@@ -644,6 +681,7 @@ var PgVectorStore = class {
|
|
|
644
681
|
dimensions;
|
|
645
682
|
/** The widths with a partial HNSW index, for a mixed-dimension table. */
|
|
646
683
|
indexedDimensions;
|
|
684
|
+
vectorType;
|
|
647
685
|
nullableEmbeddings;
|
|
648
686
|
iterativeScan;
|
|
649
687
|
efSearch;
|
|
@@ -667,6 +705,10 @@ var PgVectorStore = class {
|
|
|
667
705
|
...new Set(dimensions)
|
|
668
706
|
];
|
|
669
707
|
}
|
|
708
|
+
this.vectorType = options.vectorType ?? "vector";
|
|
709
|
+
if (this.vectorType === "halfvec" && this.dimensions === void 0) {
|
|
710
|
+
throw new Error('PgVectorStore: vectorType "halfvec" needs a single width, not a list');
|
|
711
|
+
}
|
|
670
712
|
this.nullableEmbeddings = options.nullableEmbeddings ?? false;
|
|
671
713
|
this.iterativeScan = options.iterativeScan;
|
|
672
714
|
this.efSearch = options.efSearch;
|
|
@@ -685,7 +727,7 @@ var PgVectorStore = class {
|
|
|
685
727
|
*/
|
|
686
728
|
schemaStatements() {
|
|
687
729
|
const mixed = this.dimensions === void 0;
|
|
688
|
-
const column = mixed ? "vector" :
|
|
730
|
+
const column = mixed ? "vector" : `${this.vectorType}(${this.dimensions})`;
|
|
689
731
|
const statements = [
|
|
690
732
|
"CREATE EXTENSION IF NOT EXISTS vector",
|
|
691
733
|
`CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
@@ -704,7 +746,7 @@ var PgVectorStore = class {
|
|
|
704
746
|
}
|
|
705
747
|
} else {
|
|
706
748
|
statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
|
|
707
|
-
ON ${this.table} USING hnsw (embedding
|
|
749
|
+
ON ${this.table} USING hnsw (embedding ${this.vectorType}_cosine_ops)`);
|
|
708
750
|
}
|
|
709
751
|
return statements;
|
|
710
752
|
}
|
|
@@ -720,6 +762,36 @@ var PgVectorStore = class {
|
|
|
720
762
|
* {@link PgVectorStoreOptions.nullableEmbeddings}).
|
|
721
763
|
*/
|
|
722
764
|
async upsert(records) {
|
|
765
|
+
await this.upsertWith(this.client, records);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Swaps a document's chunks in one transaction, so a crash can't lose it and a search sees the old
|
|
769
|
+
* version or the new one. A client without `transaction` falls back to two statements.
|
|
770
|
+
*
|
|
771
|
+
* Concurrent swaps of one document take turns on an advisory lock. Without it the second `DELETE`
|
|
772
|
+
* can't see the rows the first just committed, so the first's tail survives next to the second's
|
|
773
|
+
* chunks, and overlapping writes can deadlock.
|
|
774
|
+
*/
|
|
775
|
+
async replaceDocument(documentId, records) {
|
|
776
|
+
const swap = /* @__PURE__ */ __name(async (client) => {
|
|
777
|
+
await client.query(`DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1`, [
|
|
778
|
+
stripNulBytes(documentId)
|
|
779
|
+
]);
|
|
780
|
+
await this.upsertWith(client, records);
|
|
781
|
+
}, "swap");
|
|
782
|
+
const transaction = this.client.transaction?.bind(this.client);
|
|
783
|
+
if (transaction === void 0) {
|
|
784
|
+
await swap(this.client);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
await transaction(async (client) => {
|
|
788
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
789
|
+
`${this.table}:${stripNulBytes(documentId)}`
|
|
790
|
+
]);
|
|
791
|
+
await swap(client);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async upsertWith(client, records) {
|
|
723
795
|
const byId = /* @__PURE__ */ new Map();
|
|
724
796
|
for (const record of records) {
|
|
725
797
|
const id = stripNulBytes(record.id);
|
|
@@ -741,9 +813,9 @@ var PgVectorStore = class {
|
|
|
741
813
|
const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
|
|
742
814
|
const base = params.length;
|
|
743
815
|
params.push(record.id, text, source ?? null, metadata !== void 0 ? JSON.stringify(metadata) : null, record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null);
|
|
744
|
-
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}
|
|
816
|
+
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::${this.vectorType})`;
|
|
745
817
|
});
|
|
746
|
-
await
|
|
818
|
+
await client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
|
|
747
819
|
VALUES ${rows.join(", ")}
|
|
748
820
|
ON CONFLICT (id) DO UPDATE SET
|
|
749
821
|
text = EXCLUDED.text,
|
|
@@ -867,6 +939,10 @@ var PgVectorStore = class {
|
|
|
867
939
|
if (Object.keys(filter).length === 0) {
|
|
868
940
|
throw new UnsafeRemovalError("empty-filter", "removeWhere() refuses an empty filter: it would delete every chunk in the table. Pass a filter that scopes the removal, or delete deliberately with removeMany(await store.listDocumentIds()).");
|
|
869
941
|
}
|
|
942
|
+
const missing = undefinedFilterKeys(filter);
|
|
943
|
+
if (missing.length > 0) {
|
|
944
|
+
throw new UnsafeRemovalError("undefined-value", undefinedFilterMessage(missing));
|
|
945
|
+
}
|
|
870
946
|
if (filterMatchesNothing(filter)) {
|
|
871
947
|
return 0;
|
|
872
948
|
}
|
|
@@ -898,7 +974,7 @@ var PgVectorStore = class {
|
|
|
898
974
|
options.topK
|
|
899
975
|
];
|
|
900
976
|
const conditions = this.whereConditions(options.filter, params);
|
|
901
|
-
let vectorType =
|
|
977
|
+
let vectorType = this.vectorType;
|
|
902
978
|
if (this.dimensions === void 0) {
|
|
903
979
|
const width = embedding.length;
|
|
904
980
|
if (this.indexedDimensions.includes(width)) {
|
|
@@ -908,7 +984,7 @@ var PgVectorStore = class {
|
|
|
908
984
|
} else if (this.nullableEmbeddings) {
|
|
909
985
|
conditions.push("embedding IS NOT NULL");
|
|
910
986
|
}
|
|
911
|
-
const column = vectorType ===
|
|
987
|
+
const column = vectorType === this.vectorType ? "embedding" : `(embedding::${vectorType})`;
|
|
912
988
|
const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score
|
|
913
989
|
FROM ${this.table}
|
|
914
990
|
${conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -1095,6 +1171,10 @@ function buildWhere(filter, params) {
|
|
|
1095
1171
|
const clauses = [];
|
|
1096
1172
|
const scalar = {};
|
|
1097
1173
|
for (const [key, value] of Object.entries(stripped)) {
|
|
1174
|
+
if (value === void 0 || Array.isArray(value) && value.includes(void 0)) {
|
|
1175
|
+
clauses.push("false");
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1098
1178
|
if (Array.isArray(value)) {
|
|
1099
1179
|
if (value.length === 0) {
|
|
1100
1180
|
clauses.push("false");
|
|
@@ -1124,6 +1204,11 @@ function toVectorLiteral(embedding) {
|
|
|
1124
1204
|
__name(toVectorLiteral, "toVectorLiteral");
|
|
1125
1205
|
|
|
1126
1206
|
// src/redis-vector-store.ts
|
|
1207
|
+
var import_node_crypto = require("crypto");
|
|
1208
|
+
var REPLACE_LOCK_TTL_MS = 3e4;
|
|
1209
|
+
var REPLACE_LOCK_WAIT_MS = 12e4;
|
|
1210
|
+
var RENEW_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0";
|
|
1211
|
+
var RELEASE_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0";
|
|
1127
1212
|
var RedisVectorSchemaMismatchError = class extends Error {
|
|
1128
1213
|
static {
|
|
1129
1214
|
__name(this, "RedisVectorSchemaMismatchError");
|
|
@@ -1285,6 +1370,92 @@ var RedisVectorStore = class {
|
|
|
1285
1370
|
}
|
|
1286
1371
|
}
|
|
1287
1372
|
/**
|
|
1373
|
+
* Replaces a document's chunks, one writer at a time per document. Redis can't swap them in one step
|
|
1374
|
+
* without scanning the keyspace inside a script, so a search may briefly see old and new chunks
|
|
1375
|
+
* together, but concurrent calls can't leave them mixed. The new chunks go in before the old ones
|
|
1376
|
+
* come out, so a crash leaves the document searchable and the next call repairs it.
|
|
1377
|
+
*/
|
|
1378
|
+
async replaceDocument(documentId, records) {
|
|
1379
|
+
await this.withDocumentLock(documentId, async (assertHeld) => {
|
|
1380
|
+
await this.upsert(records);
|
|
1381
|
+
assertHeld();
|
|
1382
|
+
const current = new Set(records.map((record) => `${this.prefix}${record.id}`));
|
|
1383
|
+
const stale = (await this.chunkKeys(documentId)).filter((key) => !current.has(key));
|
|
1384
|
+
assertHeld();
|
|
1385
|
+
if (stale.length > 0) {
|
|
1386
|
+
await this.client.sendCommand([
|
|
1387
|
+
"DEL",
|
|
1388
|
+
...stale
|
|
1389
|
+
]);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* A lease, so a crashed writer can't block the document forever. It is renewed while the work runs,
|
|
1395
|
+
* which keeps a large document from outliving it.
|
|
1396
|
+
*/
|
|
1397
|
+
async withDocumentLock(documentId, work) {
|
|
1398
|
+
const key = `${this.index}:replace-lock:${documentId}`;
|
|
1399
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
1400
|
+
const deadline = Date.now() + REPLACE_LOCK_WAIT_MS;
|
|
1401
|
+
let delay = 10;
|
|
1402
|
+
while (await this.client.sendCommand([
|
|
1403
|
+
"SET",
|
|
1404
|
+
key,
|
|
1405
|
+
token,
|
|
1406
|
+
"NX",
|
|
1407
|
+
"PX",
|
|
1408
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1409
|
+
]) === null) {
|
|
1410
|
+
if (Date.now() >= deadline) {
|
|
1411
|
+
throw new Error(`RedisVectorStore: timed out after ${REPLACE_LOCK_WAIT_MS}ms waiting to replace document "${documentId}"`);
|
|
1412
|
+
}
|
|
1413
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1414
|
+
delay = Math.min(delay * 2, 250);
|
|
1415
|
+
}
|
|
1416
|
+
let lost = false;
|
|
1417
|
+
const assertHeld = /* @__PURE__ */ __name(() => {
|
|
1418
|
+
if (lost) {
|
|
1419
|
+
throw new Error(`RedisVectorStore: lost the lock while replacing document "${documentId}"; call again to repair it`);
|
|
1420
|
+
}
|
|
1421
|
+
}, "assertHeld");
|
|
1422
|
+
const renewal = setInterval(() => {
|
|
1423
|
+
void Promise.resolve(this.client.sendCommand([
|
|
1424
|
+
"EVAL",
|
|
1425
|
+
RENEW_LOCK_SCRIPT,
|
|
1426
|
+
"1",
|
|
1427
|
+
key,
|
|
1428
|
+
token,
|
|
1429
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1430
|
+
])).then(
|
|
1431
|
+
(renewed) => {
|
|
1432
|
+
if (Number(renewed) === 0) {
|
|
1433
|
+
lost = true;
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
// A failed renewal isn't a lost lock yet: the lease may still have time left.
|
|
1437
|
+
() => {
|
|
1438
|
+
}
|
|
1439
|
+
);
|
|
1440
|
+
}, REPLACE_LOCK_TTL_MS / 3);
|
|
1441
|
+
renewal.unref?.();
|
|
1442
|
+
try {
|
|
1443
|
+
const result = await work(assertHeld);
|
|
1444
|
+
assertHeld();
|
|
1445
|
+
return result;
|
|
1446
|
+
} finally {
|
|
1447
|
+
clearInterval(renewal);
|
|
1448
|
+
await Promise.resolve(this.client.sendCommand([
|
|
1449
|
+
"EVAL",
|
|
1450
|
+
RELEASE_LOCK_SCRIPT,
|
|
1451
|
+
"1",
|
|
1452
|
+
key,
|
|
1453
|
+
token
|
|
1454
|
+
])).catch(() => {
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1288
1459
|
* Single-document deletion, by `SCAN … MATCH`.
|
|
1289
1460
|
*
|
|
1290
1461
|
* This deliberately stays a keyspace scan even though the rest of this class no longer needs one.
|
|
@@ -1636,6 +1807,10 @@ var RedisVectorStore = class {
|
|
|
1636
1807
|
if (keys.length === 0) {
|
|
1637
1808
|
throw new UnsafeRemovalError("empty-filter", "removeWhere() refuses an empty filter: it would delete every chunk in the index. Pass a filter that scopes the removal, or delete deliberately with removeMany(await store.listDocumentIds()).");
|
|
1638
1809
|
}
|
|
1810
|
+
const missing = undefinedFilterKeys(filter);
|
|
1811
|
+
if (missing.length > 0) {
|
|
1812
|
+
throw new UnsafeRemovalError("undefined-value", undefinedFilterMessage(missing));
|
|
1813
|
+
}
|
|
1639
1814
|
const unindexed = keys.filter((key) => !this.filterableFields.includes(key));
|
|
1640
1815
|
if (unindexed.length > 0) {
|
|
1641
1816
|
const named = unindexed.map((key) => `"${key}"`).join(", ");
|
|
@@ -2507,13 +2682,19 @@ function createRetrievalTool(retriever, options = {}) {
|
|
|
2507
2682
|
inputSchema: querySchema
|
|
2508
2683
|
},
|
|
2509
2684
|
handler: {
|
|
2510
|
-
async execute(input) {
|
|
2685
|
+
async execute(input, ctx) {
|
|
2511
2686
|
if (!isQueryInput(input)) {
|
|
2512
2687
|
throw new Error("retrieval tool expects { query: string }");
|
|
2513
2688
|
}
|
|
2514
|
-
const
|
|
2515
|
-
|
|
2516
|
-
|
|
2689
|
+
const filter = options.filter?.(ctx);
|
|
2690
|
+
const passages = await instrumented.retrieve(input.query, {
|
|
2691
|
+
...topK !== void 0 ? {
|
|
2692
|
+
topK
|
|
2693
|
+
} : {},
|
|
2694
|
+
...filter !== void 0 ? {
|
|
2695
|
+
filter
|
|
2696
|
+
} : {}
|
|
2697
|
+
});
|
|
2517
2698
|
return {
|
|
2518
2699
|
passages
|
|
2519
2700
|
};
|
|
@@ -2553,7 +2734,7 @@ function openAiEmbeddings(options) {
|
|
|
2553
2734
|
const maxInputChars = options.maxInputChars ?? 24e3;
|
|
2554
2735
|
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
2555
2736
|
const doFetch = options.fetch ?? fetch;
|
|
2556
|
-
const request = /* @__PURE__ */ __name(async (input) => {
|
|
2737
|
+
const request = /* @__PURE__ */ __name(async (input, count) => {
|
|
2557
2738
|
const response = await doFetch(url, {
|
|
2558
2739
|
method: "POST",
|
|
2559
2740
|
headers: {
|
|
@@ -2586,19 +2767,20 @@ function openAiEmbeddings(options) {
|
|
|
2586
2767
|
const tokens = json2?.usage?.total_tokens ?? json2?.usage?.prompt_tokens;
|
|
2587
2768
|
if (typeof tokens === "number" && Number.isFinite(tokens)) {
|
|
2588
2769
|
options.onUsage?.(tokens);
|
|
2770
|
+
count(tokens);
|
|
2589
2771
|
}
|
|
2590
2772
|
return items.map((item) => item.embedding);
|
|
2591
2773
|
}, "request");
|
|
2592
|
-
const requestSplitting = /* @__PURE__ */ __name(async (input) => {
|
|
2774
|
+
const requestSplitting = /* @__PURE__ */ __name(async (input, count) => {
|
|
2593
2775
|
if (input.length > batchSize) {
|
|
2594
2776
|
const vectors = [];
|
|
2595
2777
|
for (let start = 0; start < input.length; start += batchSize) {
|
|
2596
|
-
vectors.push(...await requestSplitting(input.slice(start, start + batchSize)));
|
|
2778
|
+
vectors.push(...await requestSplitting(input.slice(start, start + batchSize), count));
|
|
2597
2779
|
}
|
|
2598
2780
|
return vectors;
|
|
2599
2781
|
}
|
|
2600
2782
|
try {
|
|
2601
|
-
return await request(input);
|
|
2783
|
+
return await request(input, count);
|
|
2602
2784
|
} catch (error) {
|
|
2603
2785
|
if (!isBatchTooLarge(error) || input.length <= 1) {
|
|
2604
2786
|
throw error;
|
|
@@ -2610,20 +2792,37 @@ function openAiEmbeddings(options) {
|
|
|
2610
2792
|
options.onWarn?.(`embeddings server ${url} refused a batch of ${input.length} inputs (${error.message}); retrying in batches of ${half}. Raise the server's batch limit (TEI: --max-client-batch-size) or lower batchSize.`);
|
|
2611
2793
|
}
|
|
2612
2794
|
return [
|
|
2613
|
-
...await requestSplitting(input.slice(0, half)),
|
|
2614
|
-
...await requestSplitting(input.slice(half))
|
|
2795
|
+
...await requestSplitting(input.slice(0, half), count),
|
|
2796
|
+
...await requestSplitting(input.slice(half), count)
|
|
2615
2797
|
];
|
|
2616
2798
|
}
|
|
2617
2799
|
}, "requestSplitting");
|
|
2800
|
+
const embedAll = /* @__PURE__ */ __name(async (texts, count) => {
|
|
2801
|
+
const vectors = [];
|
|
2802
|
+
for (let start = 0; start < texts.length; ) {
|
|
2803
|
+
const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
|
|
2804
|
+
vectors.push(...await requestSplitting(input, count));
|
|
2805
|
+
start += input.length;
|
|
2806
|
+
}
|
|
2807
|
+
return vectors;
|
|
2808
|
+
}, "embedAll");
|
|
2618
2809
|
return {
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
}
|
|
2626
|
-
return
|
|
2810
|
+
embed: /* @__PURE__ */ __name((texts) => embedAll(texts, () => {
|
|
2811
|
+
}), "embed"),
|
|
2812
|
+
async embedWithUsage(texts) {
|
|
2813
|
+
let inputTokens;
|
|
2814
|
+
const vectors = await embedAll(texts, (tokens) => {
|
|
2815
|
+
inputTokens = (inputTokens ?? 0) + tokens;
|
|
2816
|
+
});
|
|
2817
|
+
return {
|
|
2818
|
+
vectors,
|
|
2819
|
+
...inputTokens !== void 0 ? {
|
|
2820
|
+
usage: {
|
|
2821
|
+
inputTokens,
|
|
2822
|
+
modelId: options.model
|
|
2823
|
+
}
|
|
2824
|
+
} : {}
|
|
2825
|
+
};
|
|
2627
2826
|
}
|
|
2628
2827
|
};
|
|
2629
2828
|
}
|
|
@@ -2728,6 +2927,207 @@ function itemsOf(body) {
|
|
|
2728
2927
|
}
|
|
2729
2928
|
__name(itemsOf, "itemsOf");
|
|
2730
2929
|
|
|
2930
|
+
// src/embedding-usage.ts
|
|
2931
|
+
async function embedCountingUsage(provider, texts) {
|
|
2932
|
+
const reporting = provider;
|
|
2933
|
+
return typeof reporting.embedWithUsage === "function" ? reporting.embedWithUsage(texts) : {
|
|
2934
|
+
vectors: await provider.embed(texts)
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
__name(embedCountingUsage, "embedCountingUsage");
|
|
2938
|
+
|
|
2939
|
+
// src/rag-index.ts
|
|
2940
|
+
var RAG_ENTITY_KEY = "ragEntity";
|
|
2941
|
+
var RagScopeRequiredError = class extends Error {
|
|
2942
|
+
static {
|
|
2943
|
+
__name(this, "RagScopeRequiredError");
|
|
2944
|
+
}
|
|
2945
|
+
model;
|
|
2946
|
+
key;
|
|
2947
|
+
constructor(model, key) {
|
|
2948
|
+
super(`${model} needs scope.${key}: its chunks are isolated by it`), this.model = model, this.key = key;
|
|
2949
|
+
this.name = "RagScopeRequiredError";
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
var ANY_OF = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/anyOf");
|
|
2953
|
+
function anyOf(values) {
|
|
2954
|
+
return {
|
|
2955
|
+
[ANY_OF]: values
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
__name(anyOf, "anyOf");
|
|
2959
|
+
function isScopeValue(value) {
|
|
2960
|
+
return typeof value === "number" ? Number.isFinite(value) : typeof value === "string" && value !== "";
|
|
2961
|
+
}
|
|
2962
|
+
__name(isScopeValue, "isScopeValue");
|
|
2963
|
+
function isAnyOf(value) {
|
|
2964
|
+
return typeof value === "object" && value !== null && ANY_OF in value;
|
|
2965
|
+
}
|
|
2966
|
+
__name(isAnyOf, "isAnyOf");
|
|
2967
|
+
var RagIndex = class {
|
|
2968
|
+
static {
|
|
2969
|
+
__name(this, "RagIndex");
|
|
2970
|
+
}
|
|
2971
|
+
definition;
|
|
2972
|
+
deps;
|
|
2973
|
+
embedder;
|
|
2974
|
+
constructor(definition, deps) {
|
|
2975
|
+
this.definition = definition;
|
|
2976
|
+
this.deps = deps;
|
|
2977
|
+
const { onUsage } = deps;
|
|
2978
|
+
this.embedder = onUsage === void 0 ? deps.embedder : {
|
|
2979
|
+
embed: /* @__PURE__ */ __name(async (texts) => {
|
|
2980
|
+
const { vectors, usage } = await embedCountingUsage(deps.embedder, texts);
|
|
2981
|
+
if (usage !== void 0) {
|
|
2982
|
+
await onUsage(usage);
|
|
2983
|
+
}
|
|
2984
|
+
return vectors;
|
|
2985
|
+
}, "embed")
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
/** The id this row's chunks are stored under: `<name>:<id>`. */
|
|
2989
|
+
documentId(id) {
|
|
2990
|
+
if (!isScopeValue(id)) {
|
|
2991
|
+
throw new Error(`${this.definition.name}: a row needs an id to be indexed or removed`);
|
|
2992
|
+
}
|
|
2993
|
+
return `${this.definition.name}:${String(id)}`;
|
|
2994
|
+
}
|
|
2995
|
+
/** A `Retriever` locked to this scope, for `HybridRetriever`, `createRetrievalTool` and so on. */
|
|
2996
|
+
scoped(scope) {
|
|
2997
|
+
return new FilteredRetriever(this.baseRetriever(), this.scopeFilter(scope));
|
|
2998
|
+
}
|
|
2999
|
+
async search(query, options) {
|
|
3000
|
+
const { scope, filter, topK } = options;
|
|
3001
|
+
return this.scoped(scope).retrieve(query, {
|
|
3002
|
+
...topK !== void 0 ? {
|
|
3003
|
+
topK
|
|
3004
|
+
} : {},
|
|
3005
|
+
...filter !== void 0 ? {
|
|
3006
|
+
filter
|
|
3007
|
+
} : {}
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* Replaces the row's chunks and returns how many were written. Embeddings come first: if the
|
|
3012
|
+
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres. Redis
|
|
3013
|
+
* adds the new chunks before dropping the old, so a search can briefly see both. On a store without
|
|
3014
|
+
* `replaceDocument`, a crash mid-swap loses the document until the next `index`.
|
|
3015
|
+
*/
|
|
3016
|
+
async index(row) {
|
|
3017
|
+
const { definition, deps } = this;
|
|
3018
|
+
const documentId = this.documentId(definition.id(row));
|
|
3019
|
+
const scope = this.scopeFilter(Object.fromEntries(Object.entries(definition.scope).map(([key, read]) => [
|
|
3020
|
+
key,
|
|
3021
|
+
read(row)
|
|
3022
|
+
])));
|
|
3023
|
+
const text = definition.text(row)?.trim();
|
|
3024
|
+
if (!text) {
|
|
3025
|
+
await deps.store.remove(documentId);
|
|
3026
|
+
return 0;
|
|
3027
|
+
}
|
|
3028
|
+
const pieces = chunkText(text, definition.chunk).slice(0, definition.maxChunks);
|
|
3029
|
+
const embeddings = await this.embedder.embed(pieces);
|
|
3030
|
+
const source = definition.source?.(row) ?? void 0;
|
|
3031
|
+
const metadata = {
|
|
3032
|
+
...definition.metadata?.(row),
|
|
3033
|
+
...scope
|
|
3034
|
+
};
|
|
3035
|
+
await replaceDocumentChunks(deps.store, documentId, pieces.map((piece, index) => ({
|
|
3036
|
+
id: `${documentId}#${index}`,
|
|
3037
|
+
text: piece,
|
|
3038
|
+
embedding: embeddings[index] ?? [],
|
|
3039
|
+
...source !== void 0 ? {
|
|
3040
|
+
source
|
|
3041
|
+
} : {},
|
|
3042
|
+
metadata
|
|
3043
|
+
})));
|
|
3044
|
+
return pieces.length;
|
|
3045
|
+
}
|
|
3046
|
+
/** Takes the id itself once the row is gone. */
|
|
3047
|
+
remove(rowOrId) {
|
|
3048
|
+
const id = typeof rowOrId === "string" || typeof rowOrId === "number" ? rowOrId : this.definition.id(rowOrId);
|
|
3049
|
+
return this.deps.store.remove(this.documentId(id));
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Diffs the index against the ids that exist, and removes what no hook could: `ON DELETE CASCADE`,
|
|
3053
|
+
* raw SQL, a crash after a commit. `liveIds` must be complete for the part being compared, because
|
|
3054
|
+
* anything left out is removed. Use `dryRun` to look first.
|
|
3055
|
+
*/
|
|
3056
|
+
async reconcile(liveIds, options = {}) {
|
|
3057
|
+
const filter = {
|
|
3058
|
+
[RAG_ENTITY_KEY]: this.definition.name
|
|
3059
|
+
};
|
|
3060
|
+
for (const [key, value] of Object.entries(options.scope ?? {})) {
|
|
3061
|
+
if (!isScopeValue(value)) {
|
|
3062
|
+
throw new RagScopeRequiredError(this.definition.name, key);
|
|
3063
|
+
}
|
|
3064
|
+
filter[key] = String(value);
|
|
3065
|
+
}
|
|
3066
|
+
const live = /* @__PURE__ */ new Set();
|
|
3067
|
+
for await (const id of liveIds) {
|
|
3068
|
+
live.add(this.documentId(id));
|
|
3069
|
+
}
|
|
3070
|
+
const indexed = await this.deps.store.listDocumentIds(filter);
|
|
3071
|
+
const orphans = indexed.filter((id) => !live.has(id));
|
|
3072
|
+
if (options.dryRun !== true) {
|
|
3073
|
+
await this.deps.store.removeMany(orphans);
|
|
3074
|
+
}
|
|
3075
|
+
const known = new Set(indexed);
|
|
3076
|
+
return {
|
|
3077
|
+
orphans,
|
|
3078
|
+
missing: [
|
|
3079
|
+
...live
|
|
3080
|
+
].filter((id) => !known.has(id))
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
/** A `search_*` tool whose scope comes from the context. Pass it to `provideAgentTool`. */
|
|
3084
|
+
tool(options) {
|
|
3085
|
+
return createRetrievalTool(this.baseRetriever(), {
|
|
3086
|
+
name: options.name,
|
|
3087
|
+
...options.description !== void 0 ? {
|
|
3088
|
+
description: options.description
|
|
3089
|
+
} : {},
|
|
3090
|
+
...options.topK !== void 0 ? {
|
|
3091
|
+
topK: options.topK
|
|
3092
|
+
} : {},
|
|
3093
|
+
filter: /* @__PURE__ */ __name((ctx) => this.scopeFilter(options.scope(ctx)), "filter")
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
baseRetriever() {
|
|
3097
|
+
const { minScore } = this.definition;
|
|
3098
|
+
return this.deps.retriever ?? new EmbeddingRetriever(this.embedder, this.deps.store, minScore !== void 0 ? {
|
|
3099
|
+
minScore
|
|
3100
|
+
} : {});
|
|
3101
|
+
}
|
|
3102
|
+
/** The filter a scope compiles to. Throws on anything but a usable value, before any paid call. */
|
|
3103
|
+
scopeFilter(scope) {
|
|
3104
|
+
const given = scope ?? {};
|
|
3105
|
+
const filter = {
|
|
3106
|
+
[RAG_ENTITY_KEY]: this.definition.name
|
|
3107
|
+
};
|
|
3108
|
+
for (const key of Object.keys(this.definition.scope)) {
|
|
3109
|
+
const value = given[key];
|
|
3110
|
+
if (isAnyOf(value) && value[ANY_OF].every(isScopeValue)) {
|
|
3111
|
+
filter[key] = value[ANY_OF].map(String);
|
|
3112
|
+
} else if (isScopeValue(value)) {
|
|
3113
|
+
filter[key] = String(value);
|
|
3114
|
+
} else {
|
|
3115
|
+
throw new RagScopeRequiredError(this.definition.name, key);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
return filter;
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
function ragFilterableFields(...scopeKeys) {
|
|
3122
|
+
return [
|
|
3123
|
+
.../* @__PURE__ */ new Set([
|
|
3124
|
+
RAG_ENTITY_KEY,
|
|
3125
|
+
...scopeKeys
|
|
3126
|
+
])
|
|
3127
|
+
];
|
|
3128
|
+
}
|
|
3129
|
+
__name(ragFilterableFields, "ragFilterableFields");
|
|
3130
|
+
|
|
2731
3131
|
// src/tree/types.ts
|
|
2732
3132
|
function* walkTree(nodes, path = []) {
|
|
2733
3133
|
for (const node of nodes) {
|
|
@@ -5007,8 +5407,11 @@ __name(createNavigateDocumentTool, "createNavigateDocumentTool");
|
|
|
5007
5407
|
PgLexicalVectorStore,
|
|
5008
5408
|
PgVectorStore,
|
|
5009
5409
|
RAG_DIAGNOSTIC_LIB,
|
|
5410
|
+
RAG_ENTITY_KEY,
|
|
5010
5411
|
RAG_RETRIEVAL_CHANNEL,
|
|
5011
5412
|
RAG_RETRIEVAL_EVENT,
|
|
5413
|
+
RagIndex,
|
|
5414
|
+
RagScopeRequiredError,
|
|
5012
5415
|
RedisVectorSchemaMismatchError,
|
|
5013
5416
|
RedisVectorStore,
|
|
5014
5417
|
RerankingRetriever,
|
|
@@ -5016,6 +5419,7 @@ __name(createNavigateDocumentTool, "createNavigateDocumentTool");
|
|
|
5016
5419
|
TreeNavigationRetriever,
|
|
5017
5420
|
TwoStageRetriever,
|
|
5018
5421
|
UnsafeRemovalError,
|
|
5422
|
+
anyOf,
|
|
5019
5423
|
anyTermTsquery,
|
|
5020
5424
|
applyMetadataPatch,
|
|
5021
5425
|
buildDocumentTree,
|
|
@@ -5031,6 +5435,7 @@ __name(createNavigateDocumentTool, "createNavigateDocumentTool");
|
|
|
5031
5435
|
describesRetrieval,
|
|
5032
5436
|
detectHeadings,
|
|
5033
5437
|
documentIdOf,
|
|
5438
|
+
embedCountingUsage,
|
|
5034
5439
|
emitRagRetrieval,
|
|
5035
5440
|
estimateTokens,
|
|
5036
5441
|
hasSearchSyntax,
|
|
@@ -5046,6 +5451,8 @@ __name(createNavigateDocumentTool, "createNavigateDocumentTool");
|
|
|
5046
5451
|
openAiChatTreeLlm,
|
|
5047
5452
|
openAiEmbeddings,
|
|
5048
5453
|
parseJsonReply,
|
|
5454
|
+
ragFilterableFields,
|
|
5455
|
+
replaceDocumentChunks,
|
|
5049
5456
|
spanText,
|
|
5050
5457
|
stableHash,
|
|
5051
5458
|
toPgClient,
|