@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/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 index_exports = {};
23
- __export(index_exports, {
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(index_exports);
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" : `vector(${this.dimensions})`;
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 vector_cosine_ops)`);
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}::vector)`;
816
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::${this.vectorType})`;
745
817
  });
746
- await this.client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
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 = "vector";
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 === "vector" ? "embedding" : `(embedding::${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 passages = await instrumented.retrieve(input.query, topK !== void 0 ? {
2515
- topK
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
- async embed(texts) {
2620
- const vectors = [];
2621
- for (let start = 0; start < texts.length; ) {
2622
- const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
2623
- vectors.push(...await requestSplitting(input));
2624
- start += input.length;
2625
- }
2626
- return vectors;
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,