@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.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" : `vector(${this.dimensions})`;
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 vector_cosine_ops)`);
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}::vector)`;
728
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::${this.vectorType})`;
664
729
  });
665
- await this.client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
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 = "vector";
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 === "vector" ? "embedding" : `(embedding::${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 passages = await instrumented.retrieve(input.query, topK !== void 0 ? {
2434
- topK
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
- async embed(texts) {
2539
- const vectors = [];
2540
- for (let start = 0; start < texts.length; ) {
2541
- const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
2542
- vectors.push(...await requestSplitting(input));
2543
- start += input.length;
2544
- }
2545
- return vectors;
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,