@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.js
CHANGED
|
@@ -77,6 +77,16 @@ function chunkByRecord(text, chunkSize, separator) {
|
|
|
77
77
|
__name(chunkByRecord, "chunkByRecord");
|
|
78
78
|
|
|
79
79
|
// src/vector-store.ts
|
|
80
|
+
async function replaceDocumentChunks(store, documentId, records) {
|
|
81
|
+
const atomic = store;
|
|
82
|
+
if (typeof atomic.replaceDocument === "function") {
|
|
83
|
+
await atomic.replaceDocument(documentId, records);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await store.remove(documentId);
|
|
87
|
+
await store.upsert(records);
|
|
88
|
+
}
|
|
89
|
+
__name(replaceDocumentChunks, "replaceDocumentChunks");
|
|
80
90
|
function isLexicalVectorStore(store) {
|
|
81
91
|
return typeof store.searchText === "function";
|
|
82
92
|
}
|
|
@@ -137,12 +147,19 @@ function applyMetadataPatch(metadata, patch) {
|
|
|
137
147
|
__name(applyMetadataPatch, "applyMetadataPatch");
|
|
138
148
|
|
|
139
149
|
// src/filter.ts
|
|
150
|
+
function isMissingValue(value) {
|
|
151
|
+
return value === void 0 || Array.isArray(value) && value.includes(void 0);
|
|
152
|
+
}
|
|
153
|
+
__name(isMissingValue, "isMissingValue");
|
|
140
154
|
function matchesFilter(metadata, filter) {
|
|
141
155
|
const entries = Object.entries(filter);
|
|
142
156
|
if (metadata === void 0) {
|
|
143
157
|
return entries.length === 0;
|
|
144
158
|
}
|
|
145
159
|
return entries.every(([key, expected]) => {
|
|
160
|
+
if (isMissingValue(expected)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
146
163
|
if (Array.isArray(expected)) {
|
|
147
164
|
const actual = metadata[key];
|
|
148
165
|
return Array.isArray(actual) ? actual.some((value) => expected.includes(value)) : expected.includes(actual);
|
|
@@ -155,9 +172,18 @@ function filterMatchesNothing(filter) {
|
|
|
155
172
|
if (filter === void 0) {
|
|
156
173
|
return false;
|
|
157
174
|
}
|
|
158
|
-
return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);
|
|
175
|
+
return Object.values(filter).some((value) => isMissingValue(value) || Array.isArray(value) && value.length === 0);
|
|
159
176
|
}
|
|
160
177
|
__name(filterMatchesNothing, "filterMatchesNothing");
|
|
178
|
+
function undefinedFilterKeys(filter) {
|
|
179
|
+
return Object.entries(filter).filter(([, value]) => isMissingValue(value)).map(([key]) => key);
|
|
180
|
+
}
|
|
181
|
+
__name(undefinedFilterKeys, "undefinedFilterKeys");
|
|
182
|
+
function undefinedFilterMessage(keys) {
|
|
183
|
+
const named = keys.map((key) => `"${key}"`).join(", ");
|
|
184
|
+
return `removeWhere() refuses a filter with no value for ${named}: the scope was probably never set.`;
|
|
185
|
+
}
|
|
186
|
+
__name(undefinedFilterMessage, "undefinedFilterMessage");
|
|
161
187
|
|
|
162
188
|
// src/memory-vector-store.ts
|
|
163
189
|
var MemoryVectorStore = class {
|
|
@@ -294,6 +320,10 @@ var MemoryVectorStore = class {
|
|
|
294
320
|
if (Object.keys(filter).length === 0) {
|
|
295
321
|
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()).");
|
|
296
322
|
}
|
|
323
|
+
const missing = undefinedFilterKeys(filter);
|
|
324
|
+
if (missing.length > 0) {
|
|
325
|
+
throw new UnsafeRemovalError("undefined-value", undefinedFilterMessage(missing));
|
|
326
|
+
}
|
|
297
327
|
if (filterMatchesNothing(filter)) {
|
|
298
328
|
return 0;
|
|
299
329
|
}
|
|
@@ -563,6 +593,7 @@ var PgVectorStore = class {
|
|
|
563
593
|
dimensions;
|
|
564
594
|
/** The widths with a partial HNSW index, for a mixed-dimension table. */
|
|
565
595
|
indexedDimensions;
|
|
596
|
+
vectorType;
|
|
566
597
|
nullableEmbeddings;
|
|
567
598
|
iterativeScan;
|
|
568
599
|
efSearch;
|
|
@@ -586,6 +617,10 @@ var PgVectorStore = class {
|
|
|
586
617
|
...new Set(dimensions)
|
|
587
618
|
];
|
|
588
619
|
}
|
|
620
|
+
this.vectorType = options.vectorType ?? "vector";
|
|
621
|
+
if (this.vectorType === "halfvec" && this.dimensions === void 0) {
|
|
622
|
+
throw new Error('PgVectorStore: vectorType "halfvec" needs a single width, not a list');
|
|
623
|
+
}
|
|
589
624
|
this.nullableEmbeddings = options.nullableEmbeddings ?? false;
|
|
590
625
|
this.iterativeScan = options.iterativeScan;
|
|
591
626
|
this.efSearch = options.efSearch;
|
|
@@ -604,7 +639,7 @@ var PgVectorStore = class {
|
|
|
604
639
|
*/
|
|
605
640
|
schemaStatements() {
|
|
606
641
|
const mixed = this.dimensions === void 0;
|
|
607
|
-
const column = mixed ? "vector" :
|
|
642
|
+
const column = mixed ? "vector" : `${this.vectorType}(${this.dimensions})`;
|
|
608
643
|
const statements = [
|
|
609
644
|
"CREATE EXTENSION IF NOT EXISTS vector",
|
|
610
645
|
`CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
@@ -623,7 +658,7 @@ var PgVectorStore = class {
|
|
|
623
658
|
}
|
|
624
659
|
} else {
|
|
625
660
|
statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
|
|
626
|
-
ON ${this.table} USING hnsw (embedding
|
|
661
|
+
ON ${this.table} USING hnsw (embedding ${this.vectorType}_cosine_ops)`);
|
|
627
662
|
}
|
|
628
663
|
return statements;
|
|
629
664
|
}
|
|
@@ -639,6 +674,36 @@ var PgVectorStore = class {
|
|
|
639
674
|
* {@link PgVectorStoreOptions.nullableEmbeddings}).
|
|
640
675
|
*/
|
|
641
676
|
async upsert(records) {
|
|
677
|
+
await this.upsertWith(this.client, records);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Swaps a document's chunks in one transaction, so a crash can't lose it and a search sees the old
|
|
681
|
+
* version or the new one. A client without `transaction` falls back to two statements.
|
|
682
|
+
*
|
|
683
|
+
* Concurrent swaps of one document take turns on an advisory lock. Without it the second `DELETE`
|
|
684
|
+
* can't see the rows the first just committed, so the first's tail survives next to the second's
|
|
685
|
+
* chunks, and overlapping writes can deadlock.
|
|
686
|
+
*/
|
|
687
|
+
async replaceDocument(documentId, records) {
|
|
688
|
+
const swap = /* @__PURE__ */ __name(async (client) => {
|
|
689
|
+
await client.query(`DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1`, [
|
|
690
|
+
stripNulBytes(documentId)
|
|
691
|
+
]);
|
|
692
|
+
await this.upsertWith(client, records);
|
|
693
|
+
}, "swap");
|
|
694
|
+
const transaction = this.client.transaction?.bind(this.client);
|
|
695
|
+
if (transaction === void 0) {
|
|
696
|
+
await swap(this.client);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
await transaction(async (client) => {
|
|
700
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
701
|
+
`${this.table}:${stripNulBytes(documentId)}`
|
|
702
|
+
]);
|
|
703
|
+
await swap(client);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
async upsertWith(client, records) {
|
|
642
707
|
const byId = /* @__PURE__ */ new Map();
|
|
643
708
|
for (const record of records) {
|
|
644
709
|
const id = stripNulBytes(record.id);
|
|
@@ -660,9 +725,9 @@ var PgVectorStore = class {
|
|
|
660
725
|
const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
|
|
661
726
|
const base = params.length;
|
|
662
727
|
params.push(record.id, text, source ?? null, metadata !== void 0 ? JSON.stringify(metadata) : null, record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null);
|
|
663
|
-
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}
|
|
728
|
+
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::${this.vectorType})`;
|
|
664
729
|
});
|
|
665
|
-
await
|
|
730
|
+
await client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
|
|
666
731
|
VALUES ${rows.join(", ")}
|
|
667
732
|
ON CONFLICT (id) DO UPDATE SET
|
|
668
733
|
text = EXCLUDED.text,
|
|
@@ -786,6 +851,10 @@ var PgVectorStore = class {
|
|
|
786
851
|
if (Object.keys(filter).length === 0) {
|
|
787
852
|
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()).");
|
|
788
853
|
}
|
|
854
|
+
const missing = undefinedFilterKeys(filter);
|
|
855
|
+
if (missing.length > 0) {
|
|
856
|
+
throw new UnsafeRemovalError("undefined-value", undefinedFilterMessage(missing));
|
|
857
|
+
}
|
|
789
858
|
if (filterMatchesNothing(filter)) {
|
|
790
859
|
return 0;
|
|
791
860
|
}
|
|
@@ -817,7 +886,7 @@ var PgVectorStore = class {
|
|
|
817
886
|
options.topK
|
|
818
887
|
];
|
|
819
888
|
const conditions = this.whereConditions(options.filter, params);
|
|
820
|
-
let vectorType =
|
|
889
|
+
let vectorType = this.vectorType;
|
|
821
890
|
if (this.dimensions === void 0) {
|
|
822
891
|
const width = embedding.length;
|
|
823
892
|
if (this.indexedDimensions.includes(width)) {
|
|
@@ -827,7 +896,7 @@ var PgVectorStore = class {
|
|
|
827
896
|
} else if (this.nullableEmbeddings) {
|
|
828
897
|
conditions.push("embedding IS NOT NULL");
|
|
829
898
|
}
|
|
830
|
-
const column = vectorType ===
|
|
899
|
+
const column = vectorType === this.vectorType ? "embedding" : `(embedding::${vectorType})`;
|
|
831
900
|
const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score
|
|
832
901
|
FROM ${this.table}
|
|
833
902
|
${conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""}
|
|
@@ -1014,6 +1083,10 @@ function buildWhere(filter, params) {
|
|
|
1014
1083
|
const clauses = [];
|
|
1015
1084
|
const scalar = {};
|
|
1016
1085
|
for (const [key, value] of Object.entries(stripped)) {
|
|
1086
|
+
if (value === void 0 || Array.isArray(value) && value.includes(void 0)) {
|
|
1087
|
+
clauses.push("false");
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1017
1090
|
if (Array.isArray(value)) {
|
|
1018
1091
|
if (value.length === 0) {
|
|
1019
1092
|
clauses.push("false");
|
|
@@ -1043,6 +1116,11 @@ function toVectorLiteral(embedding) {
|
|
|
1043
1116
|
__name(toVectorLiteral, "toVectorLiteral");
|
|
1044
1117
|
|
|
1045
1118
|
// src/redis-vector-store.ts
|
|
1119
|
+
import { randomUUID } from "crypto";
|
|
1120
|
+
var REPLACE_LOCK_TTL_MS = 3e4;
|
|
1121
|
+
var REPLACE_LOCK_WAIT_MS = 12e4;
|
|
1122
|
+
var RENEW_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0";
|
|
1123
|
+
var RELEASE_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0";
|
|
1046
1124
|
var RedisVectorSchemaMismatchError = class extends Error {
|
|
1047
1125
|
static {
|
|
1048
1126
|
__name(this, "RedisVectorSchemaMismatchError");
|
|
@@ -1204,6 +1282,92 @@ var RedisVectorStore = class {
|
|
|
1204
1282
|
}
|
|
1205
1283
|
}
|
|
1206
1284
|
/**
|
|
1285
|
+
* Replaces a document's chunks, one writer at a time per document. Redis can't swap them in one step
|
|
1286
|
+
* without scanning the keyspace inside a script, so a search may briefly see old and new chunks
|
|
1287
|
+
* together, but concurrent calls can't leave them mixed. The new chunks go in before the old ones
|
|
1288
|
+
* come out, so a crash leaves the document searchable and the next call repairs it.
|
|
1289
|
+
*/
|
|
1290
|
+
async replaceDocument(documentId, records) {
|
|
1291
|
+
await this.withDocumentLock(documentId, async (assertHeld) => {
|
|
1292
|
+
await this.upsert(records);
|
|
1293
|
+
assertHeld();
|
|
1294
|
+
const current = new Set(records.map((record) => `${this.prefix}${record.id}`));
|
|
1295
|
+
const stale = (await this.chunkKeys(documentId)).filter((key) => !current.has(key));
|
|
1296
|
+
assertHeld();
|
|
1297
|
+
if (stale.length > 0) {
|
|
1298
|
+
await this.client.sendCommand([
|
|
1299
|
+
"DEL",
|
|
1300
|
+
...stale
|
|
1301
|
+
]);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* A lease, so a crashed writer can't block the document forever. It is renewed while the work runs,
|
|
1307
|
+
* which keeps a large document from outliving it.
|
|
1308
|
+
*/
|
|
1309
|
+
async withDocumentLock(documentId, work) {
|
|
1310
|
+
const key = `${this.index}:replace-lock:${documentId}`;
|
|
1311
|
+
const token = randomUUID();
|
|
1312
|
+
const deadline = Date.now() + REPLACE_LOCK_WAIT_MS;
|
|
1313
|
+
let delay = 10;
|
|
1314
|
+
while (await this.client.sendCommand([
|
|
1315
|
+
"SET",
|
|
1316
|
+
key,
|
|
1317
|
+
token,
|
|
1318
|
+
"NX",
|
|
1319
|
+
"PX",
|
|
1320
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1321
|
+
]) === null) {
|
|
1322
|
+
if (Date.now() >= deadline) {
|
|
1323
|
+
throw new Error(`RedisVectorStore: timed out after ${REPLACE_LOCK_WAIT_MS}ms waiting to replace document "${documentId}"`);
|
|
1324
|
+
}
|
|
1325
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1326
|
+
delay = Math.min(delay * 2, 250);
|
|
1327
|
+
}
|
|
1328
|
+
let lost = false;
|
|
1329
|
+
const assertHeld = /* @__PURE__ */ __name(() => {
|
|
1330
|
+
if (lost) {
|
|
1331
|
+
throw new Error(`RedisVectorStore: lost the lock while replacing document "${documentId}"; call again to repair it`);
|
|
1332
|
+
}
|
|
1333
|
+
}, "assertHeld");
|
|
1334
|
+
const renewal = setInterval(() => {
|
|
1335
|
+
void Promise.resolve(this.client.sendCommand([
|
|
1336
|
+
"EVAL",
|
|
1337
|
+
RENEW_LOCK_SCRIPT,
|
|
1338
|
+
"1",
|
|
1339
|
+
key,
|
|
1340
|
+
token,
|
|
1341
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1342
|
+
])).then(
|
|
1343
|
+
(renewed) => {
|
|
1344
|
+
if (Number(renewed) === 0) {
|
|
1345
|
+
lost = true;
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
// A failed renewal isn't a lost lock yet: the lease may still have time left.
|
|
1349
|
+
() => {
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
}, REPLACE_LOCK_TTL_MS / 3);
|
|
1353
|
+
renewal.unref?.();
|
|
1354
|
+
try {
|
|
1355
|
+
const result = await work(assertHeld);
|
|
1356
|
+
assertHeld();
|
|
1357
|
+
return result;
|
|
1358
|
+
} finally {
|
|
1359
|
+
clearInterval(renewal);
|
|
1360
|
+
await Promise.resolve(this.client.sendCommand([
|
|
1361
|
+
"EVAL",
|
|
1362
|
+
RELEASE_LOCK_SCRIPT,
|
|
1363
|
+
"1",
|
|
1364
|
+
key,
|
|
1365
|
+
token
|
|
1366
|
+
])).catch(() => {
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1207
1371
|
* Single-document deletion, by `SCAN … MATCH`.
|
|
1208
1372
|
*
|
|
1209
1373
|
* This deliberately stays a keyspace scan even though the rest of this class no longer needs one.
|
|
@@ -1555,6 +1719,10 @@ var RedisVectorStore = class {
|
|
|
1555
1719
|
if (keys.length === 0) {
|
|
1556
1720
|
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()).");
|
|
1557
1721
|
}
|
|
1722
|
+
const missing = undefinedFilterKeys(filter);
|
|
1723
|
+
if (missing.length > 0) {
|
|
1724
|
+
throw new UnsafeRemovalError("undefined-value", undefinedFilterMessage(missing));
|
|
1725
|
+
}
|
|
1558
1726
|
const unindexed = keys.filter((key) => !this.filterableFields.includes(key));
|
|
1559
1727
|
if (unindexed.length > 0) {
|
|
1560
1728
|
const named = unindexed.map((key) => `"${key}"`).join(", ");
|
|
@@ -2426,13 +2594,19 @@ function createRetrievalTool(retriever, options = {}) {
|
|
|
2426
2594
|
inputSchema: querySchema
|
|
2427
2595
|
},
|
|
2428
2596
|
handler: {
|
|
2429
|
-
async execute(input) {
|
|
2597
|
+
async execute(input, ctx) {
|
|
2430
2598
|
if (!isQueryInput(input)) {
|
|
2431
2599
|
throw new Error("retrieval tool expects { query: string }");
|
|
2432
2600
|
}
|
|
2433
|
-
const
|
|
2434
|
-
|
|
2435
|
-
|
|
2601
|
+
const filter = options.filter?.(ctx);
|
|
2602
|
+
const passages = await instrumented.retrieve(input.query, {
|
|
2603
|
+
...topK !== void 0 ? {
|
|
2604
|
+
topK
|
|
2605
|
+
} : {},
|
|
2606
|
+
...filter !== void 0 ? {
|
|
2607
|
+
filter
|
|
2608
|
+
} : {}
|
|
2609
|
+
});
|
|
2436
2610
|
return {
|
|
2437
2611
|
passages
|
|
2438
2612
|
};
|
|
@@ -2472,7 +2646,7 @@ function openAiEmbeddings(options) {
|
|
|
2472
2646
|
const maxInputChars = options.maxInputChars ?? 24e3;
|
|
2473
2647
|
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
2474
2648
|
const doFetch = options.fetch ?? fetch;
|
|
2475
|
-
const request = /* @__PURE__ */ __name(async (input) => {
|
|
2649
|
+
const request = /* @__PURE__ */ __name(async (input, count) => {
|
|
2476
2650
|
const response = await doFetch(url, {
|
|
2477
2651
|
method: "POST",
|
|
2478
2652
|
headers: {
|
|
@@ -2505,19 +2679,20 @@ function openAiEmbeddings(options) {
|
|
|
2505
2679
|
const tokens = json2?.usage?.total_tokens ?? json2?.usage?.prompt_tokens;
|
|
2506
2680
|
if (typeof tokens === "number" && Number.isFinite(tokens)) {
|
|
2507
2681
|
options.onUsage?.(tokens);
|
|
2682
|
+
count(tokens);
|
|
2508
2683
|
}
|
|
2509
2684
|
return items.map((item) => item.embedding);
|
|
2510
2685
|
}, "request");
|
|
2511
|
-
const requestSplitting = /* @__PURE__ */ __name(async (input) => {
|
|
2686
|
+
const requestSplitting = /* @__PURE__ */ __name(async (input, count) => {
|
|
2512
2687
|
if (input.length > batchSize) {
|
|
2513
2688
|
const vectors = [];
|
|
2514
2689
|
for (let start = 0; start < input.length; start += batchSize) {
|
|
2515
|
-
vectors.push(...await requestSplitting(input.slice(start, start + batchSize)));
|
|
2690
|
+
vectors.push(...await requestSplitting(input.slice(start, start + batchSize), count));
|
|
2516
2691
|
}
|
|
2517
2692
|
return vectors;
|
|
2518
2693
|
}
|
|
2519
2694
|
try {
|
|
2520
|
-
return await request(input);
|
|
2695
|
+
return await request(input, count);
|
|
2521
2696
|
} catch (error) {
|
|
2522
2697
|
if (!isBatchTooLarge(error) || input.length <= 1) {
|
|
2523
2698
|
throw error;
|
|
@@ -2529,20 +2704,37 @@ function openAiEmbeddings(options) {
|
|
|
2529
2704
|
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.`);
|
|
2530
2705
|
}
|
|
2531
2706
|
return [
|
|
2532
|
-
...await requestSplitting(input.slice(0, half)),
|
|
2533
|
-
...await requestSplitting(input.slice(half))
|
|
2707
|
+
...await requestSplitting(input.slice(0, half), count),
|
|
2708
|
+
...await requestSplitting(input.slice(half), count)
|
|
2534
2709
|
];
|
|
2535
2710
|
}
|
|
2536
2711
|
}, "requestSplitting");
|
|
2712
|
+
const embedAll = /* @__PURE__ */ __name(async (texts, count) => {
|
|
2713
|
+
const vectors = [];
|
|
2714
|
+
for (let start = 0; start < texts.length; ) {
|
|
2715
|
+
const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
|
|
2716
|
+
vectors.push(...await requestSplitting(input, count));
|
|
2717
|
+
start += input.length;
|
|
2718
|
+
}
|
|
2719
|
+
return vectors;
|
|
2720
|
+
}, "embedAll");
|
|
2537
2721
|
return {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
}
|
|
2545
|
-
return
|
|
2722
|
+
embed: /* @__PURE__ */ __name((texts) => embedAll(texts, () => {
|
|
2723
|
+
}), "embed"),
|
|
2724
|
+
async embedWithUsage(texts) {
|
|
2725
|
+
let inputTokens;
|
|
2726
|
+
const vectors = await embedAll(texts, (tokens) => {
|
|
2727
|
+
inputTokens = (inputTokens ?? 0) + tokens;
|
|
2728
|
+
});
|
|
2729
|
+
return {
|
|
2730
|
+
vectors,
|
|
2731
|
+
...inputTokens !== void 0 ? {
|
|
2732
|
+
usage: {
|
|
2733
|
+
inputTokens,
|
|
2734
|
+
modelId: options.model
|
|
2735
|
+
}
|
|
2736
|
+
} : {}
|
|
2737
|
+
};
|
|
2546
2738
|
}
|
|
2547
2739
|
};
|
|
2548
2740
|
}
|
|
@@ -2647,6 +2839,207 @@ function itemsOf(body) {
|
|
|
2647
2839
|
}
|
|
2648
2840
|
__name(itemsOf, "itemsOf");
|
|
2649
2841
|
|
|
2842
|
+
// src/embedding-usage.ts
|
|
2843
|
+
async function embedCountingUsage(provider, texts) {
|
|
2844
|
+
const reporting = provider;
|
|
2845
|
+
return typeof reporting.embedWithUsage === "function" ? reporting.embedWithUsage(texts) : {
|
|
2846
|
+
vectors: await provider.embed(texts)
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
__name(embedCountingUsage, "embedCountingUsage");
|
|
2850
|
+
|
|
2851
|
+
// src/rag-index.ts
|
|
2852
|
+
var RAG_ENTITY_KEY = "ragEntity";
|
|
2853
|
+
var RagScopeRequiredError = class extends Error {
|
|
2854
|
+
static {
|
|
2855
|
+
__name(this, "RagScopeRequiredError");
|
|
2856
|
+
}
|
|
2857
|
+
model;
|
|
2858
|
+
key;
|
|
2859
|
+
constructor(model, key) {
|
|
2860
|
+
super(`${model} needs scope.${key}: its chunks are isolated by it`), this.model = model, this.key = key;
|
|
2861
|
+
this.name = "RagScopeRequiredError";
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
var ANY_OF = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/anyOf");
|
|
2865
|
+
function anyOf(values) {
|
|
2866
|
+
return {
|
|
2867
|
+
[ANY_OF]: values
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
__name(anyOf, "anyOf");
|
|
2871
|
+
function isScopeValue(value) {
|
|
2872
|
+
return typeof value === "number" ? Number.isFinite(value) : typeof value === "string" && value !== "";
|
|
2873
|
+
}
|
|
2874
|
+
__name(isScopeValue, "isScopeValue");
|
|
2875
|
+
function isAnyOf(value) {
|
|
2876
|
+
return typeof value === "object" && value !== null && ANY_OF in value;
|
|
2877
|
+
}
|
|
2878
|
+
__name(isAnyOf, "isAnyOf");
|
|
2879
|
+
var RagIndex = class {
|
|
2880
|
+
static {
|
|
2881
|
+
__name(this, "RagIndex");
|
|
2882
|
+
}
|
|
2883
|
+
definition;
|
|
2884
|
+
deps;
|
|
2885
|
+
embedder;
|
|
2886
|
+
constructor(definition, deps) {
|
|
2887
|
+
this.definition = definition;
|
|
2888
|
+
this.deps = deps;
|
|
2889
|
+
const { onUsage } = deps;
|
|
2890
|
+
this.embedder = onUsage === void 0 ? deps.embedder : {
|
|
2891
|
+
embed: /* @__PURE__ */ __name(async (texts) => {
|
|
2892
|
+
const { vectors, usage } = await embedCountingUsage(deps.embedder, texts);
|
|
2893
|
+
if (usage !== void 0) {
|
|
2894
|
+
await onUsage(usage);
|
|
2895
|
+
}
|
|
2896
|
+
return vectors;
|
|
2897
|
+
}, "embed")
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
/** The id this row's chunks are stored under: `<name>:<id>`. */
|
|
2901
|
+
documentId(id) {
|
|
2902
|
+
if (!isScopeValue(id)) {
|
|
2903
|
+
throw new Error(`${this.definition.name}: a row needs an id to be indexed or removed`);
|
|
2904
|
+
}
|
|
2905
|
+
return `${this.definition.name}:${String(id)}`;
|
|
2906
|
+
}
|
|
2907
|
+
/** A `Retriever` locked to this scope, for `HybridRetriever`, `createRetrievalTool` and so on. */
|
|
2908
|
+
scoped(scope) {
|
|
2909
|
+
return new FilteredRetriever(this.baseRetriever(), this.scopeFilter(scope));
|
|
2910
|
+
}
|
|
2911
|
+
async search(query, options) {
|
|
2912
|
+
const { scope, filter, topK } = options;
|
|
2913
|
+
return this.scoped(scope).retrieve(query, {
|
|
2914
|
+
...topK !== void 0 ? {
|
|
2915
|
+
topK
|
|
2916
|
+
} : {},
|
|
2917
|
+
...filter !== void 0 ? {
|
|
2918
|
+
filter
|
|
2919
|
+
} : {}
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Replaces the row's chunks and returns how many were written. Embeddings come first: if the
|
|
2924
|
+
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres. Redis
|
|
2925
|
+
* adds the new chunks before dropping the old, so a search can briefly see both. On a store without
|
|
2926
|
+
* `replaceDocument`, a crash mid-swap loses the document until the next `index`.
|
|
2927
|
+
*/
|
|
2928
|
+
async index(row) {
|
|
2929
|
+
const { definition, deps } = this;
|
|
2930
|
+
const documentId = this.documentId(definition.id(row));
|
|
2931
|
+
const scope = this.scopeFilter(Object.fromEntries(Object.entries(definition.scope).map(([key, read]) => [
|
|
2932
|
+
key,
|
|
2933
|
+
read(row)
|
|
2934
|
+
])));
|
|
2935
|
+
const text = definition.text(row)?.trim();
|
|
2936
|
+
if (!text) {
|
|
2937
|
+
await deps.store.remove(documentId);
|
|
2938
|
+
return 0;
|
|
2939
|
+
}
|
|
2940
|
+
const pieces = chunkText(text, definition.chunk).slice(0, definition.maxChunks);
|
|
2941
|
+
const embeddings = await this.embedder.embed(pieces);
|
|
2942
|
+
const source = definition.source?.(row) ?? void 0;
|
|
2943
|
+
const metadata = {
|
|
2944
|
+
...definition.metadata?.(row),
|
|
2945
|
+
...scope
|
|
2946
|
+
};
|
|
2947
|
+
await replaceDocumentChunks(deps.store, documentId, pieces.map((piece, index) => ({
|
|
2948
|
+
id: `${documentId}#${index}`,
|
|
2949
|
+
text: piece,
|
|
2950
|
+
embedding: embeddings[index] ?? [],
|
|
2951
|
+
...source !== void 0 ? {
|
|
2952
|
+
source
|
|
2953
|
+
} : {},
|
|
2954
|
+
metadata
|
|
2955
|
+
})));
|
|
2956
|
+
return pieces.length;
|
|
2957
|
+
}
|
|
2958
|
+
/** Takes the id itself once the row is gone. */
|
|
2959
|
+
remove(rowOrId) {
|
|
2960
|
+
const id = typeof rowOrId === "string" || typeof rowOrId === "number" ? rowOrId : this.definition.id(rowOrId);
|
|
2961
|
+
return this.deps.store.remove(this.documentId(id));
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Diffs the index against the ids that exist, and removes what no hook could: `ON DELETE CASCADE`,
|
|
2965
|
+
* raw SQL, a crash after a commit. `liveIds` must be complete for the part being compared, because
|
|
2966
|
+
* anything left out is removed. Use `dryRun` to look first.
|
|
2967
|
+
*/
|
|
2968
|
+
async reconcile(liveIds, options = {}) {
|
|
2969
|
+
const filter = {
|
|
2970
|
+
[RAG_ENTITY_KEY]: this.definition.name
|
|
2971
|
+
};
|
|
2972
|
+
for (const [key, value] of Object.entries(options.scope ?? {})) {
|
|
2973
|
+
if (!isScopeValue(value)) {
|
|
2974
|
+
throw new RagScopeRequiredError(this.definition.name, key);
|
|
2975
|
+
}
|
|
2976
|
+
filter[key] = String(value);
|
|
2977
|
+
}
|
|
2978
|
+
const live = /* @__PURE__ */ new Set();
|
|
2979
|
+
for await (const id of liveIds) {
|
|
2980
|
+
live.add(this.documentId(id));
|
|
2981
|
+
}
|
|
2982
|
+
const indexed = await this.deps.store.listDocumentIds(filter);
|
|
2983
|
+
const orphans = indexed.filter((id) => !live.has(id));
|
|
2984
|
+
if (options.dryRun !== true) {
|
|
2985
|
+
await this.deps.store.removeMany(orphans);
|
|
2986
|
+
}
|
|
2987
|
+
const known = new Set(indexed);
|
|
2988
|
+
return {
|
|
2989
|
+
orphans,
|
|
2990
|
+
missing: [
|
|
2991
|
+
...live
|
|
2992
|
+
].filter((id) => !known.has(id))
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
/** A `search_*` tool whose scope comes from the context. Pass it to `provideAgentTool`. */
|
|
2996
|
+
tool(options) {
|
|
2997
|
+
return createRetrievalTool(this.baseRetriever(), {
|
|
2998
|
+
name: options.name,
|
|
2999
|
+
...options.description !== void 0 ? {
|
|
3000
|
+
description: options.description
|
|
3001
|
+
} : {},
|
|
3002
|
+
...options.topK !== void 0 ? {
|
|
3003
|
+
topK: options.topK
|
|
3004
|
+
} : {},
|
|
3005
|
+
filter: /* @__PURE__ */ __name((ctx) => this.scopeFilter(options.scope(ctx)), "filter")
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
baseRetriever() {
|
|
3009
|
+
const { minScore } = this.definition;
|
|
3010
|
+
return this.deps.retriever ?? new EmbeddingRetriever(this.embedder, this.deps.store, minScore !== void 0 ? {
|
|
3011
|
+
minScore
|
|
3012
|
+
} : {});
|
|
3013
|
+
}
|
|
3014
|
+
/** The filter a scope compiles to. Throws on anything but a usable value, before any paid call. */
|
|
3015
|
+
scopeFilter(scope) {
|
|
3016
|
+
const given = scope ?? {};
|
|
3017
|
+
const filter = {
|
|
3018
|
+
[RAG_ENTITY_KEY]: this.definition.name
|
|
3019
|
+
};
|
|
3020
|
+
for (const key of Object.keys(this.definition.scope)) {
|
|
3021
|
+
const value = given[key];
|
|
3022
|
+
if (isAnyOf(value) && value[ANY_OF].every(isScopeValue)) {
|
|
3023
|
+
filter[key] = value[ANY_OF].map(String);
|
|
3024
|
+
} else if (isScopeValue(value)) {
|
|
3025
|
+
filter[key] = String(value);
|
|
3026
|
+
} else {
|
|
3027
|
+
throw new RagScopeRequiredError(this.definition.name, key);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
return filter;
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
3033
|
+
function ragFilterableFields(...scopeKeys) {
|
|
3034
|
+
return [
|
|
3035
|
+
.../* @__PURE__ */ new Set([
|
|
3036
|
+
RAG_ENTITY_KEY,
|
|
3037
|
+
...scopeKeys
|
|
3038
|
+
])
|
|
3039
|
+
];
|
|
3040
|
+
}
|
|
3041
|
+
__name(ragFilterableFields, "ragFilterableFields");
|
|
3042
|
+
|
|
2650
3043
|
// src/tree/types.ts
|
|
2651
3044
|
function* walkTree(nodes, path = []) {
|
|
2652
3045
|
for (const node of nodes) {
|
|
@@ -4925,8 +5318,11 @@ export {
|
|
|
4925
5318
|
PgLexicalVectorStore,
|
|
4926
5319
|
PgVectorStore,
|
|
4927
5320
|
RAG_DIAGNOSTIC_LIB,
|
|
5321
|
+
RAG_ENTITY_KEY,
|
|
4928
5322
|
RAG_RETRIEVAL_CHANNEL,
|
|
4929
5323
|
RAG_RETRIEVAL_EVENT,
|
|
5324
|
+
RagIndex,
|
|
5325
|
+
RagScopeRequiredError,
|
|
4930
5326
|
RedisVectorSchemaMismatchError,
|
|
4931
5327
|
RedisVectorStore,
|
|
4932
5328
|
RerankingRetriever,
|
|
@@ -4934,6 +5330,7 @@ export {
|
|
|
4934
5330
|
TreeNavigationRetriever,
|
|
4935
5331
|
TwoStageRetriever,
|
|
4936
5332
|
UnsafeRemovalError,
|
|
5333
|
+
anyOf,
|
|
4937
5334
|
anyTermTsquery,
|
|
4938
5335
|
applyMetadataPatch,
|
|
4939
5336
|
buildDocumentTree,
|
|
@@ -4949,6 +5346,7 @@ export {
|
|
|
4949
5346
|
describesRetrieval,
|
|
4950
5347
|
detectHeadings,
|
|
4951
5348
|
documentIdOf,
|
|
5349
|
+
embedCountingUsage,
|
|
4952
5350
|
emitRagRetrieval,
|
|
4953
5351
|
estimateTokens,
|
|
4954
5352
|
hasSearchSyntax,
|
|
@@ -4964,6 +5362,8 @@ export {
|
|
|
4964
5362
|
openAiChatTreeLlm,
|
|
4965
5363
|
openAiEmbeddings,
|
|
4966
5364
|
parseJsonReply,
|
|
5365
|
+
ragFilterableFields,
|
|
5366
|
+
replaceDocumentChunks,
|
|
4967
5367
|
spanText,
|
|
4968
5368
|
stableHash,
|
|
4969
5369
|
toPgClient,
|