@dudousxd/nestjs-agent-rag 0.10.2 → 0.11.0

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
@@ -23,10 +23,13 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  EmbeddingRetriever: () => EmbeddingRetriever,
25
25
  FilteredRetriever: () => FilteredRetriever,
26
+ HttpModelError: () => HttpModelError,
27
+ HttpReranker: () => HttpReranker,
26
28
  HybridRetriever: () => HybridRetriever,
27
29
  KeywordRetriever: () => KeywordRetriever,
28
30
  LexicalRetriever: () => LexicalRetriever,
29
31
  MemoryVectorStore: () => MemoryVectorStore,
32
+ PgLexicalVectorStore: () => PgLexicalVectorStore,
30
33
  PgVectorStore: () => PgVectorStore,
31
34
  RAG_DIAGNOSTIC_LIB: () => RAG_DIAGNOSTIC_LIB,
32
35
  RAG_RETRIEVAL_CHANNEL: () => RAG_RETRIEVAL_CHANNEL,
@@ -49,7 +52,9 @@ __export(index_exports, {
49
52
  ingestChunks: () => ingestChunks,
50
53
  ingestDocuments: () => ingestDocuments,
51
54
  instrumentRetriever: () => instrumentRetriever,
52
- isLexicalVectorStore: () => isLexicalVectorStore
55
+ isLexicalVectorStore: () => isLexicalVectorStore,
56
+ openAiEmbeddings: () => openAiEmbeddings,
57
+ toPgClient: () => toPgClient
53
58
  });
54
59
  module.exports = __toCommonJS(index_exports);
55
60
 
@@ -408,6 +413,106 @@ function cosineSimilarity(a, b) {
408
413
  }
409
414
  __name(cosineSimilarity, "cosineSimilarity");
410
415
 
416
+ // src/pg-client.ts
417
+ var ADAPTED = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/pg-client");
418
+ function toPgClient(source) {
419
+ if (source[ADAPTED]) {
420
+ return source;
421
+ }
422
+ if (isDrizzle(source)) {
423
+ return toPgClient(source.$client);
424
+ }
425
+ if (typeof source.unsafe === "function") {
426
+ return fromPostgresJs(source);
427
+ }
428
+ return fromQueryable(source);
429
+ }
430
+ __name(toPgClient, "toPgClient");
431
+ function isDrizzle(source) {
432
+ const client = source.$client;
433
+ return client !== null && client !== void 0 && typeof client === "object";
434
+ }
435
+ __name(isDrizzle, "isDrizzle");
436
+ function mark(client) {
437
+ Object.defineProperty(client, ADAPTED, {
438
+ value: true
439
+ });
440
+ return client;
441
+ }
442
+ __name(mark, "mark");
443
+ function rowsOf(result) {
444
+ if (Array.isArray(result)) {
445
+ return result;
446
+ }
447
+ const rows = result?.rows;
448
+ return Array.isArray(rows) ? rows : [];
449
+ }
450
+ __name(rowsOf, "rowsOf");
451
+ function fromPostgresJs(sql) {
452
+ const client = {
453
+ query: /* @__PURE__ */ __name(async (text, params = []) => (
454
+ // postgres.js resolves a RowList (an Array subclass); copy it to a plain array.
455
+ [
456
+ ...rowsOf(await sql.unsafe(text, params))
457
+ ]
458
+ ), "query")
459
+ };
460
+ if (typeof sql.begin === "function") {
461
+ const begin = sql.begin.bind(sql);
462
+ client.transaction = (work) => Promise.resolve(begin((tx) => work(fromPostgresJs(tx))));
463
+ }
464
+ return mark(client);
465
+ }
466
+ __name(fromPostgresJs, "fromPostgresJs");
467
+ function fromQueryable(source) {
468
+ const client = {
469
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await source.query(text, params)), "query")
470
+ };
471
+ if (typeof source.transaction === "function") {
472
+ const transaction = source.transaction.bind(source);
473
+ client.transaction = (work) => transaction((tx) => work(toPgClient(tx)));
474
+ } else if (isNodePgPool(source)) {
475
+ client.transaction = async (work) => {
476
+ const connection = await source.connect();
477
+ let broken = false;
478
+ try {
479
+ return await runInTransaction(connection, work, () => {
480
+ broken = true;
481
+ });
482
+ } finally {
483
+ connection.release(broken ? new Error("ROLLBACK failed") : void 0);
484
+ }
485
+ };
486
+ } else if (isNodePgClient(source)) {
487
+ client.transaction = (work) => runInTransaction(source, work);
488
+ }
489
+ return mark(client);
490
+ }
491
+ __name(fromQueryable, "fromQueryable");
492
+ async function runInTransaction(connection, work, onRollbackFailed) {
493
+ const tx = mark({
494
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await connection.query(text, params)), "query")
495
+ });
496
+ await connection.query("BEGIN");
497
+ try {
498
+ const result = await work(tx);
499
+ await connection.query("COMMIT");
500
+ return result;
501
+ } catch (error) {
502
+ await connection.query("ROLLBACK").catch(() => onRollbackFailed?.());
503
+ throw error;
504
+ }
505
+ }
506
+ __name(runInTransaction, "runInTransaction");
507
+ function isNodePgPool(source) {
508
+ return typeof source.connect === "function" && typeof source.totalCount === "number";
509
+ }
510
+ __name(isNodePgPool, "isNodePgPool");
511
+ function isNodePgClient(source) {
512
+ return typeof source.release === "function" || typeof source.processID === "number" || source.connectionParameters !== void 0;
513
+ }
514
+ __name(isNodePgClient, "isNodePgClient");
515
+
411
516
  // src/pg-vector-store.ts
412
517
  var NUL_BYTE = String.fromCharCode(0);
413
518
  function stripNulBytes(value) {
@@ -436,11 +541,37 @@ var PgVectorStore = class {
436
541
  }
437
542
  client;
438
543
  table;
544
+ /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */
439
545
  dimensions;
546
+ /** The widths with a partial HNSW index, for a mixed-dimension table. */
547
+ indexedDimensions;
548
+ nullableEmbeddings;
549
+ iterativeScan;
550
+ efSearch;
551
+ upsertBatchSize;
552
+ iterativeScanSupport;
440
553
  constructor(client, options = {}) {
441
- this.client = client;
554
+ this.client = toPgClient(client);
442
555
  this.table = options.table ?? "agent_rag_chunks";
443
- this.dimensions = options.dimensions ?? 1536;
556
+ const dimensions = options.dimensions ?? 1536;
557
+ if (typeof dimensions === "number") {
558
+ this.dimensions = dimensions;
559
+ this.indexedDimensions = [];
560
+ } else {
561
+ for (const width of dimensions) {
562
+ if (!Number.isInteger(width) || width <= 0) {
563
+ throw new Error(`PgVectorStore: invalid embedding width ${String(width)}`);
564
+ }
565
+ }
566
+ this.dimensions = void 0;
567
+ this.indexedDimensions = [
568
+ ...new Set(dimensions)
569
+ ];
570
+ }
571
+ this.nullableEmbeddings = options.nullableEmbeddings ?? false;
572
+ this.iterativeScan = options.iterativeScan;
573
+ this.efSearch = options.efSearch;
574
+ this.upsertBatchSize = Math.max(1, Math.floor(options.upsertBatchSize ?? 100));
444
575
  }
445
576
  /** Telemetry self-description — the table is the namespace a retrieval was served from. */
446
577
  describeRetrieval() {
@@ -449,38 +580,77 @@ var PgVectorStore = class {
449
580
  collection: this.table
450
581
  };
451
582
  }
452
- /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
453
- async ensureSchema() {
454
- await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
455
- await this.client.query(`CREATE TABLE IF NOT EXISTS ${this.table} (
583
+ /**
584
+ * The idempotent DDL {@link PgVectorStore.ensureSchema} runs, in order — for a migration instead
585
+ * of a boot-time side effect. On a populated table, run the index statements `CONCURRENTLY` there.
586
+ */
587
+ schemaStatements() {
588
+ const mixed = this.dimensions === void 0;
589
+ const column = mixed ? "vector" : `vector(${this.dimensions})`;
590
+ const statements = [
591
+ "CREATE EXTENSION IF NOT EXISTS vector",
592
+ `CREATE TABLE IF NOT EXISTS ${this.table} (
456
593
  id TEXT PRIMARY KEY,
457
594
  text TEXT NOT NULL,
458
595
  source TEXT,
459
596
  metadata JSONB,
460
- embedding vector(${this.dimensions}) NOT NULL
461
- )`);
462
- await this.client.query(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
597
+ embedding ${column}${this.nullableEmbeddings ? "" : " NOT NULL"}
598
+ )`
599
+ ];
600
+ if (mixed) {
601
+ for (const width of this.indexedDimensions) {
602
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_${width}_idx
603
+ ON ${this.table} USING hnsw ((embedding::vector(${width})) vector_cosine_ops)
604
+ WHERE vector_dims(embedding) = ${width}`);
605
+ }
606
+ } else {
607
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
463
608
  ON ${this.table} USING hnsw (embedding vector_cosine_ops)`);
609
+ }
610
+ return statements;
464
611
  }
612
+ /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index(es). */
613
+ async ensureSchema() {
614
+ for (const statement of this.schemaStatements()) {
615
+ await this.client.query(statement);
616
+ }
617
+ }
618
+ /**
619
+ * Multi-row `INSERT … ON CONFLICT (id) DO UPDATE`, {@link PgVectorStoreOptions.upsertBatchSize}
620
+ * rows per statement. An empty `embedding` is written as `NULL` (see
621
+ * {@link PgVectorStoreOptions.nullableEmbeddings}).
622
+ */
465
623
  async upsert(records) {
624
+ const byId = /* @__PURE__ */ new Map();
466
625
  for (const record of records) {
467
626
  const id = stripNulBytes(record.id);
468
- const text = stripNulBytes(record.text);
469
- const source = record.source !== void 0 ? stripNulBytes(record.source) : void 0;
470
- const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
627
+ byId.delete(id);
628
+ byId.set(id, {
629
+ ...record,
630
+ id
631
+ });
632
+ }
633
+ const unique = [
634
+ ...byId.values()
635
+ ];
636
+ for (let start = 0; start < unique.length; start += this.upsertBatchSize) {
637
+ const batch = unique.slice(start, start + this.upsertBatchSize);
638
+ const params = [];
639
+ const rows = batch.map((record) => {
640
+ const text = stripNulBytes(record.text);
641
+ const source = record.source !== void 0 ? stripNulBytes(record.source) : void 0;
642
+ const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
643
+ const base = params.length;
644
+ params.push(record.id, text, source ?? null, metadata !== void 0 ? JSON.stringify(metadata) : null, record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null);
645
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::vector)`;
646
+ });
471
647
  await this.client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
472
- VALUES ($1, $2, $3, $4, $5)
648
+ VALUES ${rows.join(", ")}
473
649
  ON CONFLICT (id) DO UPDATE SET
474
650
  text = EXCLUDED.text,
475
651
  source = EXCLUDED.source,
476
652
  metadata = EXCLUDED.metadata,
477
- embedding = EXCLUDED.embedding`, [
478
- id,
479
- text,
480
- source ?? null,
481
- metadata !== void 0 ? JSON.stringify(metadata) : null,
482
- toVectorLiteral(record.embedding)
483
- ]);
653
+ embedding = EXCLUDED.embedding`, params);
484
654
  }
485
655
  }
486
656
  async remove(documentId) {
@@ -516,11 +686,12 @@ var PgVectorStore = class {
516
686
  return rows.length;
517
687
  }
518
688
  async listDocuments(filter) {
519
- const where = buildWhere(filter, 1);
689
+ const params = [];
690
+ const where = this.where(filter, params);
520
691
  const rows = await this.client.query(`SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata
521
692
  FROM ${this.table}
522
- ${where.sql}
523
- ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, where.params);
693
+ ${where}
694
+ ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, params);
524
695
  return rows.map((row) => ({
525
696
  id: row.doc_id,
526
697
  ...row.metadata !== null ? {
@@ -571,11 +742,12 @@ var PgVectorStore = class {
571
742
  if (filterMatchesNothing(filter)) {
572
743
  return [];
573
744
  }
574
- const where = buildWhere(filter, 1);
745
+ const params = [];
746
+ const where = this.where(filter, params);
575
747
  const rows = await this.client.query(`SELECT DISTINCT ${DOCUMENT_ID_FROM_CHUNK} AS doc_id
576
748
  FROM ${this.table}
577
- ${where.sql}
578
- ORDER BY doc_id`, where.params);
749
+ ${where}
750
+ ORDER BY doc_id`, params);
579
751
  return rows.map((row) => row.doc_id);
580
752
  }
581
753
  /** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
@@ -599,81 +771,207 @@ var PgVectorStore = class {
599
771
  if (filterMatchesNothing(filter)) {
600
772
  return 0;
601
773
  }
602
- const where = buildWhere(filter, 1);
603
- const rows = await this.client.query(`DELETE FROM ${this.table} ${where.sql} RETURNING id`, where.params);
774
+ const params = [];
775
+ const where = this.where(filter, params);
776
+ const rows = await this.client.query(`DELETE FROM ${this.table} ${where} RETURNING id`, params);
604
777
  return rows.length;
605
778
  }
606
779
  async countChunks(filter) {
607
780
  if (filterMatchesNothing(filter)) {
608
781
  return 0;
609
782
  }
610
- const where = buildWhere(filter, 1);
611
- const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where.sql}`, where.params);
783
+ const params = [];
784
+ const where = this.where(filter, params);
785
+ const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where}`, params);
612
786
  return Number(rows[0]?.count ?? 0);
613
787
  }
788
+ /**
789
+ * Cosine similarity, nearest first. On a mixed-dimension table only vectors as wide as the query
790
+ * are compared (through that width's partial index when it has one); chunks without an embedding
791
+ * are never returned.
792
+ */
614
793
  async search(embedding, options) {
615
- const vector = toVectorLiteral(embedding);
616
- const where = buildWhere(options.filter, 3);
617
- const rows = await this.client.query(`SELECT id, text, source, metadata, 1 - (embedding <=> $1::vector) AS score
794
+ if (embedding.length === 0) {
795
+ return [];
796
+ }
797
+ const params = [
798
+ toVectorLiteral(embedding),
799
+ options.topK
800
+ ];
801
+ const conditions = this.whereConditions(options.filter, params);
802
+ let vectorType = "vector";
803
+ if (this.dimensions === void 0) {
804
+ const width = embedding.length;
805
+ if (this.indexedDimensions.includes(width)) {
806
+ vectorType = `vector(${width})`;
807
+ }
808
+ conditions.push("embedding IS NOT NULL", `vector_dims(embedding) = ${width}`);
809
+ } else if (this.nullableEmbeddings) {
810
+ conditions.push("embedding IS NOT NULL");
811
+ }
812
+ const column = vectorType === "vector" ? "embedding" : `(embedding::${vectorType})`;
813
+ const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score
618
814
  FROM ${this.table}
619
- ${where.sql}
620
- ORDER BY embedding <=> $1::vector
621
- LIMIT $2`, [
622
- vector,
623
- options.topK,
624
- ...where.params
625
- ]);
626
- return rows.map((row) => ({
627
- id: row.id,
628
- text: row.text,
629
- score: Number(row.score),
630
- ...row.source !== null ? {
631
- source: row.source
632
- } : {},
633
- ...row.metadata !== null ? {
634
- metadata: row.metadata
635
- } : {}
636
- }));
815
+ ${conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""}
816
+ ORDER BY ${column} <=> $1::${vectorType}
817
+ LIMIT $2`;
818
+ const rows = await this.withSearchSettings((client) => client.query(sql, params));
819
+ return rows.map(toPassage);
820
+ }
821
+ /**
822
+ * The SQL conditions (ANDed) a metadata `filter` compiles to, appending their bindings to `params`
823
+ * (placeholders are numbered after what `params` already holds). Every statement that takes a
824
+ * filter goes through here, so a subclass that maps the filter onto its own columns — a tenant id,
825
+ * an access list — overrides this one method. Return `['false']` to deny.
826
+ */
827
+ whereConditions(filter, params) {
828
+ return buildWhere(filter, params);
829
+ }
830
+ where(filter, params) {
831
+ const conditions = this.whereConditions(filter, params);
832
+ return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
833
+ }
834
+ /** Run a read with the configured HNSW settings `SET LOCAL` in one transaction, when possible. */
835
+ async withSearchSettings(read) {
836
+ const transaction = this.client.transaction?.bind(this.client);
837
+ if (transaction === void 0) {
838
+ return read(this.client);
839
+ }
840
+ const iterative = this.iterativeScan !== void 0 && await this.supportsIterativeScan();
841
+ if (!iterative && this.efSearch === void 0) {
842
+ return read(this.client);
843
+ }
844
+ return transaction(async (tx) => {
845
+ if (iterative) {
846
+ await tx.query(`SELECT set_config('hnsw.iterative_scan', $1, true)`, [
847
+ this.iterativeScan
848
+ ]);
849
+ }
850
+ if (this.efSearch !== void 0) {
851
+ await tx.query(`SELECT set_config('hnsw.ef_search', $1, true)`, [
852
+ String(this.efSearch)
853
+ ]);
854
+ }
855
+ return read(tx);
856
+ });
857
+ }
858
+ /** `hnsw.iterative_scan` exists from pgvector 0.8.0; setting it on an older one is an error. */
859
+ supportsIterativeScan() {
860
+ this.iterativeScanSupport ??= this.client.query(`SELECT extversion FROM pg_extension WHERE extname = 'vector'`).then((rows) => {
861
+ const [major = 0, minor = 0] = (rows[0]?.extversion ?? "0.0").split(".").map(Number);
862
+ return major > 0 || minor >= 8;
863
+ }).catch(() => false);
864
+ return this.iterativeScanSupport;
637
865
  }
638
866
  };
639
- function buildWhere(filter, startIndex) {
867
+ var PgLexicalVectorStore = class extends PgVectorStore {
868
+ static {
869
+ __name(this, "PgLexicalVectorStore");
870
+ }
871
+ tsConfig;
872
+ tsColumn;
873
+ anyTermFallback;
874
+ constructor(client, options = {}) {
875
+ super(client, options);
876
+ const config = options.fullText?.config ?? "simple";
877
+ if (!SQL_IDENTIFIER.test(config)) {
878
+ throw new Error(`PgLexicalVectorStore: invalid text search configuration "${config}"`);
879
+ }
880
+ const column = options.fullText?.column;
881
+ if (column !== void 0 && !SQL_IDENTIFIER.test(column)) {
882
+ throw new Error(`PgLexicalVectorStore: invalid tsvector column "${column}"`);
883
+ }
884
+ this.tsConfig = config;
885
+ this.tsColumn = column;
886
+ this.anyTermFallback = options.fullText?.anyTermFallback ?? true;
887
+ }
888
+ schemaStatements() {
889
+ const statements = super.schemaStatements();
890
+ if (this.tsColumn === void 0) {
891
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_text_tsv_idx
892
+ ON ${this.table} USING gin (${this.tsvector()})`);
893
+ }
894
+ return statements;
895
+ }
896
+ async searchText(query, options) {
897
+ if (filterMatchesNothing(options.filter) || query.trim() === "") {
898
+ return [];
899
+ }
900
+ const all = await this.runTextSearch(`websearch_to_tsquery('${this.tsConfig}', $1)`, query, options);
901
+ if (all.length > 0 || !this.anyTermFallback) {
902
+ return all;
903
+ }
904
+ const terms = [
905
+ ...new Set(query.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [])
906
+ ].slice(0, 24);
907
+ if (terms.length < 2) {
908
+ return all;
909
+ }
910
+ return this.runTextSearch(`to_tsquery('${this.tsConfig}', $1)`, terms.map((term) => `'${term}'`).join(" | "), options);
911
+ }
912
+ async runTextSearch(tsquery, query, options) {
913
+ const params = [
914
+ query,
915
+ options.topK
916
+ ];
917
+ const conditions = [
918
+ `${this.tsvector()} @@ ${tsquery}`,
919
+ ...this.whereConditions(options.filter, params)
920
+ ];
921
+ const rows = await this.client.query(`SELECT id, text, source, metadata, ts_rank_cd(${this.tsvector()}, ${tsquery}) AS score
922
+ FROM ${this.table}
923
+ WHERE ${conditions.join(" AND ")}
924
+ ORDER BY score DESC, id
925
+ LIMIT $2`, params);
926
+ return rows.map(toPassage);
927
+ }
928
+ /** Must be character-for-character the index expression, or the planner won't use the index. */
929
+ tsvector() {
930
+ return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;
931
+ }
932
+ };
933
+ var SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
934
+ function toPassage(row) {
935
+ return {
936
+ id: row.id,
937
+ text: row.text,
938
+ score: Number(row.score),
939
+ ...row.source !== null ? {
940
+ source: row.source
941
+ } : {},
942
+ ...row.metadata !== null ? {
943
+ metadata: row.metadata
944
+ } : {}
945
+ };
946
+ }
947
+ __name(toPassage, "toPassage");
948
+ function buildWhere(filter, params) {
640
949
  if (filter === void 0 || Object.keys(filter).length === 0) {
641
- return {
642
- sql: "",
643
- params: []
644
- };
950
+ return [];
645
951
  }
646
952
  const stripped = stripNulBytes(filter);
647
953
  const clauses = [];
648
- const params = [];
649
954
  const scalar = {};
650
- let index = startIndex;
651
955
  for (const [key, value] of Object.entries(stripped)) {
652
956
  if (Array.isArray(value)) {
653
957
  if (value.length === 0) {
654
958
  clauses.push("false");
655
959
  continue;
656
960
  }
657
- const keyParam = `$${index}`;
658
961
  params.push(key);
659
- index += 1;
660
- const arrParam = `$${index}`;
962
+ const keyParam = `$${params.length}`;
661
963
  params.push(value.map(String));
662
- index += 1;
964
+ const arrParam = `$${params.length}`;
663
965
  clauses.push(`(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`);
664
966
  } else {
665
967
  scalar[key] = value;
666
968
  }
667
969
  }
668
970
  if (Object.keys(scalar).length > 0) {
669
- clauses.push(`metadata @> $${index}::jsonb`);
670
971
  params.push(JSON.stringify(scalar));
671
- index += 1;
972
+ clauses.push(`metadata @> $${params.length}::jsonb`);
672
973
  }
673
- return {
674
- sql: `WHERE ${clauses.join(" AND ")}`,
675
- params
676
- };
974
+ return clauses;
677
975
  }
678
976
  __name(buildWhere, "buildWhere");
679
977
  var DOCUMENT_ID_FROM_CHUNK = "regexp_replace(id, '#[0-9]+$', '')";
@@ -1483,7 +1781,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1483
1781
  attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);
1484
1782
  }
1485
1783
  const inlineScore = scoreSource === "lexical" ? toStr(reply[index + 1]) : void 0;
1486
- passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1784
+ passages.push(toPassage2(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1487
1785
  }
1488
1786
  }
1489
1787
  return passages;
@@ -1491,7 +1789,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1491
1789
  if (typeof reply === "object" && reply !== null && "results" in reply) {
1492
1790
  const results = reply.results;
1493
1791
  if (Array.isArray(results)) {
1494
- return results.map((result) => toPassage(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1792
+ return results.map((result) => toPassage2(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1495
1793
  }
1496
1794
  }
1497
1795
  return [];
@@ -1524,7 +1822,7 @@ function readAttributes(result) {
1524
1822
  return attrs;
1525
1823
  }
1526
1824
  __name(readAttributes, "readAttributes");
1527
- function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1825
+ function toPassage2(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1528
1826
  const score = scoreSource === "lexical" ? Number(inlineScore ?? "0") : 1 - Number(attrs.vector_score ?? "1");
1529
1827
  const source = attrs.source;
1530
1828
  return {
@@ -1537,7 +1835,7 @@ function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1537
1835
  ...parseMetadata(attrs.metadata_json)
1538
1836
  };
1539
1837
  }
1540
- __name(toPassage, "toPassage");
1838
+ __name(toPassage2, "toPassage");
1541
1839
  function parseMetadata(raw) {
1542
1840
  if (raw === void 0) {
1543
1841
  return {};
@@ -2082,14 +2380,181 @@ function createRetrievalTool(retriever, options = {}) {
2082
2380
  };
2083
2381
  }
2084
2382
  __name(createRetrievalTool, "createRetrievalTool");
2383
+
2384
+ // src/openai-embeddings.ts
2385
+ var HttpModelError = class extends Error {
2386
+ static {
2387
+ __name(this, "HttpModelError");
2388
+ }
2389
+ status;
2390
+ constructor(status, message) {
2391
+ super(message), this.status = status;
2392
+ this.name = "HttpModelError";
2393
+ }
2394
+ };
2395
+ function openAiEmbeddings(options) {
2396
+ const url = `${(options.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/embeddings`;
2397
+ const batchSize = Math.max(1, Math.floor(options.batchSize ?? 64));
2398
+ const maxInputChars = options.maxInputChars ?? 24e3;
2399
+ const timeoutMs = options.timeoutMs ?? 12e4;
2400
+ const doFetch = options.fetch ?? fetch;
2401
+ return {
2402
+ async embed(texts) {
2403
+ const vectors = [];
2404
+ for (let start = 0; start < texts.length; start += batchSize) {
2405
+ const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
2406
+ const response = await doFetch(url, {
2407
+ method: "POST",
2408
+ headers: {
2409
+ "content-type": "application/json",
2410
+ ...options.apiKey ? {
2411
+ authorization: `Bearer ${options.apiKey}`
2412
+ } : {},
2413
+ ...options.headers
2414
+ },
2415
+ body: JSON.stringify({
2416
+ model: options.model,
2417
+ input,
2418
+ ...options.dimensions !== void 0 ? {
2419
+ dimensions: options.dimensions
2420
+ } : {}
2421
+ }),
2422
+ signal: AbortSignal.timeout(timeoutMs)
2423
+ });
2424
+ const body = await response.text();
2425
+ if (!response.ok) {
2426
+ throw new HttpModelError(response.status, `Embeddings request failed (${response.status}): ${errorMessageOf(body)}`);
2427
+ }
2428
+ const json = parseJson(body);
2429
+ const items = [
2430
+ ...json?.data ?? []
2431
+ ].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
2432
+ if (items.length !== input.length || !items.every((item) => Array.isArray(item.embedding))) {
2433
+ throw new HttpModelError(502, `Embeddings returned ${items.length} vectors for ${input.length} inputs`);
2434
+ }
2435
+ for (const item of items) {
2436
+ vectors.push(item.embedding);
2437
+ }
2438
+ const tokens = json?.usage?.total_tokens ?? json?.usage?.prompt_tokens;
2439
+ if (typeof tokens === "number" && Number.isFinite(tokens)) {
2440
+ options.onUsage?.(tokens);
2441
+ }
2442
+ }
2443
+ return vectors;
2444
+ }
2445
+ };
2446
+ }
2447
+ __name(openAiEmbeddings, "openAiEmbeddings");
2448
+ function parseJson(body) {
2449
+ try {
2450
+ return JSON.parse(body);
2451
+ } catch {
2452
+ return null;
2453
+ }
2454
+ }
2455
+ __name(parseJson, "parseJson");
2456
+ function errorMessageOf(body) {
2457
+ const json = parseJson(body);
2458
+ const message = typeof json?.error === "string" ? json.error : json?.error?.message ?? json?.message ?? void 0;
2459
+ return typeof message === "string" ? message : body.slice(0, 300);
2460
+ }
2461
+ __name(errorMessageOf, "errorMessageOf");
2462
+
2463
+ // src/http-reranker.ts
2464
+ var HttpReranker = class {
2465
+ static {
2466
+ __name(this, "HttpReranker");
2467
+ }
2468
+ options;
2469
+ format;
2470
+ doFetch;
2471
+ constructor(options) {
2472
+ this.options = options;
2473
+ this.format = options.format ?? "auto";
2474
+ this.doFetch = options.fetch ?? fetch;
2475
+ }
2476
+ async rerank(query, passages, options = {}) {
2477
+ if (passages.length === 0) {
2478
+ return [];
2479
+ }
2480
+ const topK = options.topK ?? passages.length;
2481
+ const documents = passages.map((passage) => passage.text);
2482
+ const response = await this.doFetch(this.options.url, {
2483
+ method: "POST",
2484
+ headers: {
2485
+ "content-type": "application/json",
2486
+ ...this.options.apiKey ? {
2487
+ authorization: `Bearer ${this.options.apiKey}`
2488
+ } : {},
2489
+ ...this.options.headers
2490
+ },
2491
+ body: JSON.stringify({
2492
+ ...this.options.model !== void 0 ? {
2493
+ model: this.options.model
2494
+ } : {},
2495
+ query,
2496
+ ...this.format !== "tei" ? {
2497
+ documents,
2498
+ top_n: topK
2499
+ } : {},
2500
+ ...this.format !== "cohere" ? {
2501
+ texts: documents
2502
+ } : {}
2503
+ }),
2504
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
2505
+ });
2506
+ const body = await response.text();
2507
+ if (!response.ok) {
2508
+ throw new HttpModelError(response.status, `Rerank request failed (${response.status}): ${errorMessageOf(body)}`);
2509
+ }
2510
+ const items = itemsOf(body);
2511
+ if (items === void 0) {
2512
+ throw new HttpModelError(502, "Rerank response has no results");
2513
+ }
2514
+ const seen = /* @__PURE__ */ new Set();
2515
+ const reranked = [];
2516
+ for (const item of items) {
2517
+ const index = item.index;
2518
+ const passage = typeof index === "number" ? passages[index] : void 0;
2519
+ if (passage === void 0 || seen.has(index)) {
2520
+ continue;
2521
+ }
2522
+ seen.add(index);
2523
+ reranked.push({
2524
+ ...passage,
2525
+ score: Number(item.relevance_score ?? item.score ?? 0)
2526
+ });
2527
+ }
2528
+ reranked.sort((a, b) => b.score - a.score);
2529
+ return reranked.slice(0, topK);
2530
+ }
2531
+ };
2532
+ function itemsOf(body) {
2533
+ let json;
2534
+ try {
2535
+ json = JSON.parse(body);
2536
+ } catch {
2537
+ return void 0;
2538
+ }
2539
+ if (Array.isArray(json)) {
2540
+ return json;
2541
+ }
2542
+ const wrapped = json;
2543
+ const items = wrapped?.results ?? wrapped?.data;
2544
+ return Array.isArray(items) ? items : void 0;
2545
+ }
2546
+ __name(itemsOf, "itemsOf");
2085
2547
  // Annotate the CommonJS export names for ESM import in node:
2086
2548
  0 && (module.exports = {
2087
2549
  EmbeddingRetriever,
2088
2550
  FilteredRetriever,
2551
+ HttpModelError,
2552
+ HttpReranker,
2089
2553
  HybridRetriever,
2090
2554
  KeywordRetriever,
2091
2555
  LexicalRetriever,
2092
2556
  MemoryVectorStore,
2557
+ PgLexicalVectorStore,
2093
2558
  PgVectorStore,
2094
2559
  RAG_DIAGNOSTIC_LIB,
2095
2560
  RAG_RETRIEVAL_CHANNEL,
@@ -2112,6 +2577,8 @@ __name(createRetrievalTool, "createRetrievalTool");
2112
2577
  ingestChunks,
2113
2578
  ingestDocuments,
2114
2579
  instrumentRetriever,
2115
- isLexicalVectorStore
2580
+ isLexicalVectorStore,
2581
+ openAiEmbeddings,
2582
+ toPgClient
2116
2583
  });
2117
2584
  //# sourceMappingURL=index.cjs.map