@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.js CHANGED
@@ -356,6 +356,106 @@ function cosineSimilarity(a, b) {
356
356
  }
357
357
  __name(cosineSimilarity, "cosineSimilarity");
358
358
 
359
+ // src/pg-client.ts
360
+ var ADAPTED = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/pg-client");
361
+ function toPgClient(source) {
362
+ if (source[ADAPTED]) {
363
+ return source;
364
+ }
365
+ if (isDrizzle(source)) {
366
+ return toPgClient(source.$client);
367
+ }
368
+ if (typeof source.unsafe === "function") {
369
+ return fromPostgresJs(source);
370
+ }
371
+ return fromQueryable(source);
372
+ }
373
+ __name(toPgClient, "toPgClient");
374
+ function isDrizzle(source) {
375
+ const client = source.$client;
376
+ return client !== null && client !== void 0 && typeof client === "object";
377
+ }
378
+ __name(isDrizzle, "isDrizzle");
379
+ function mark(client) {
380
+ Object.defineProperty(client, ADAPTED, {
381
+ value: true
382
+ });
383
+ return client;
384
+ }
385
+ __name(mark, "mark");
386
+ function rowsOf(result) {
387
+ if (Array.isArray(result)) {
388
+ return result;
389
+ }
390
+ const rows = result?.rows;
391
+ return Array.isArray(rows) ? rows : [];
392
+ }
393
+ __name(rowsOf, "rowsOf");
394
+ function fromPostgresJs(sql) {
395
+ const client = {
396
+ query: /* @__PURE__ */ __name(async (text, params = []) => (
397
+ // postgres.js resolves a RowList (an Array subclass); copy it to a plain array.
398
+ [
399
+ ...rowsOf(await sql.unsafe(text, params))
400
+ ]
401
+ ), "query")
402
+ };
403
+ if (typeof sql.begin === "function") {
404
+ const begin = sql.begin.bind(sql);
405
+ client.transaction = (work) => Promise.resolve(begin((tx) => work(fromPostgresJs(tx))));
406
+ }
407
+ return mark(client);
408
+ }
409
+ __name(fromPostgresJs, "fromPostgresJs");
410
+ function fromQueryable(source) {
411
+ const client = {
412
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await source.query(text, params)), "query")
413
+ };
414
+ if (typeof source.transaction === "function") {
415
+ const transaction = source.transaction.bind(source);
416
+ client.transaction = (work) => transaction((tx) => work(toPgClient(tx)));
417
+ } else if (isNodePgPool(source)) {
418
+ client.transaction = async (work) => {
419
+ const connection = await source.connect();
420
+ let broken = false;
421
+ try {
422
+ return await runInTransaction(connection, work, () => {
423
+ broken = true;
424
+ });
425
+ } finally {
426
+ connection.release(broken ? new Error("ROLLBACK failed") : void 0);
427
+ }
428
+ };
429
+ } else if (isNodePgClient(source)) {
430
+ client.transaction = (work) => runInTransaction(source, work);
431
+ }
432
+ return mark(client);
433
+ }
434
+ __name(fromQueryable, "fromQueryable");
435
+ async function runInTransaction(connection, work, onRollbackFailed) {
436
+ const tx = mark({
437
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await connection.query(text, params)), "query")
438
+ });
439
+ await connection.query("BEGIN");
440
+ try {
441
+ const result = await work(tx);
442
+ await connection.query("COMMIT");
443
+ return result;
444
+ } catch (error) {
445
+ await connection.query("ROLLBACK").catch(() => onRollbackFailed?.());
446
+ throw error;
447
+ }
448
+ }
449
+ __name(runInTransaction, "runInTransaction");
450
+ function isNodePgPool(source) {
451
+ return typeof source.connect === "function" && typeof source.totalCount === "number";
452
+ }
453
+ __name(isNodePgPool, "isNodePgPool");
454
+ function isNodePgClient(source) {
455
+ return typeof source.release === "function" || typeof source.processID === "number" || source.connectionParameters !== void 0;
456
+ }
457
+ __name(isNodePgClient, "isNodePgClient");
458
+
359
459
  // src/pg-vector-store.ts
360
460
  var NUL_BYTE = String.fromCharCode(0);
361
461
  function stripNulBytes(value) {
@@ -384,11 +484,37 @@ var PgVectorStore = class {
384
484
  }
385
485
  client;
386
486
  table;
487
+ /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */
387
488
  dimensions;
489
+ /** The widths with a partial HNSW index, for a mixed-dimension table. */
490
+ indexedDimensions;
491
+ nullableEmbeddings;
492
+ iterativeScan;
493
+ efSearch;
494
+ upsertBatchSize;
495
+ iterativeScanSupport;
388
496
  constructor(client, options = {}) {
389
- this.client = client;
497
+ this.client = toPgClient(client);
390
498
  this.table = options.table ?? "agent_rag_chunks";
391
- this.dimensions = options.dimensions ?? 1536;
499
+ const dimensions = options.dimensions ?? 1536;
500
+ if (typeof dimensions === "number") {
501
+ this.dimensions = dimensions;
502
+ this.indexedDimensions = [];
503
+ } else {
504
+ for (const width of dimensions) {
505
+ if (!Number.isInteger(width) || width <= 0) {
506
+ throw new Error(`PgVectorStore: invalid embedding width ${String(width)}`);
507
+ }
508
+ }
509
+ this.dimensions = void 0;
510
+ this.indexedDimensions = [
511
+ ...new Set(dimensions)
512
+ ];
513
+ }
514
+ this.nullableEmbeddings = options.nullableEmbeddings ?? false;
515
+ this.iterativeScan = options.iterativeScan;
516
+ this.efSearch = options.efSearch;
517
+ this.upsertBatchSize = Math.max(1, Math.floor(options.upsertBatchSize ?? 100));
392
518
  }
393
519
  /** Telemetry self-description — the table is the namespace a retrieval was served from. */
394
520
  describeRetrieval() {
@@ -397,38 +523,77 @@ var PgVectorStore = class {
397
523
  collection: this.table
398
524
  };
399
525
  }
400
- /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
401
- async ensureSchema() {
402
- await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
403
- await this.client.query(`CREATE TABLE IF NOT EXISTS ${this.table} (
526
+ /**
527
+ * The idempotent DDL {@link PgVectorStore.ensureSchema} runs, in order — for a migration instead
528
+ * of a boot-time side effect. On a populated table, run the index statements `CONCURRENTLY` there.
529
+ */
530
+ schemaStatements() {
531
+ const mixed = this.dimensions === void 0;
532
+ const column = mixed ? "vector" : `vector(${this.dimensions})`;
533
+ const statements = [
534
+ "CREATE EXTENSION IF NOT EXISTS vector",
535
+ `CREATE TABLE IF NOT EXISTS ${this.table} (
404
536
  id TEXT PRIMARY KEY,
405
537
  text TEXT NOT NULL,
406
538
  source TEXT,
407
539
  metadata JSONB,
408
- embedding vector(${this.dimensions}) NOT NULL
409
- )`);
410
- await this.client.query(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
540
+ embedding ${column}${this.nullableEmbeddings ? "" : " NOT NULL"}
541
+ )`
542
+ ];
543
+ if (mixed) {
544
+ for (const width of this.indexedDimensions) {
545
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_${width}_idx
546
+ ON ${this.table} USING hnsw ((embedding::vector(${width})) vector_cosine_ops)
547
+ WHERE vector_dims(embedding) = ${width}`);
548
+ }
549
+ } else {
550
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
411
551
  ON ${this.table} USING hnsw (embedding vector_cosine_ops)`);
552
+ }
553
+ return statements;
412
554
  }
555
+ /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index(es). */
556
+ async ensureSchema() {
557
+ for (const statement of this.schemaStatements()) {
558
+ await this.client.query(statement);
559
+ }
560
+ }
561
+ /**
562
+ * Multi-row `INSERT … ON CONFLICT (id) DO UPDATE`, {@link PgVectorStoreOptions.upsertBatchSize}
563
+ * rows per statement. An empty `embedding` is written as `NULL` (see
564
+ * {@link PgVectorStoreOptions.nullableEmbeddings}).
565
+ */
413
566
  async upsert(records) {
567
+ const byId = /* @__PURE__ */ new Map();
414
568
  for (const record of records) {
415
569
  const id = stripNulBytes(record.id);
416
- const text = stripNulBytes(record.text);
417
- const source = record.source !== void 0 ? stripNulBytes(record.source) : void 0;
418
- const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
570
+ byId.delete(id);
571
+ byId.set(id, {
572
+ ...record,
573
+ id
574
+ });
575
+ }
576
+ const unique = [
577
+ ...byId.values()
578
+ ];
579
+ for (let start = 0; start < unique.length; start += this.upsertBatchSize) {
580
+ const batch = unique.slice(start, start + this.upsertBatchSize);
581
+ const params = [];
582
+ const rows = batch.map((record) => {
583
+ const text = stripNulBytes(record.text);
584
+ const source = record.source !== void 0 ? stripNulBytes(record.source) : void 0;
585
+ const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
586
+ const base = params.length;
587
+ params.push(record.id, text, source ?? null, metadata !== void 0 ? JSON.stringify(metadata) : null, record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null);
588
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::vector)`;
589
+ });
419
590
  await this.client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
420
- VALUES ($1, $2, $3, $4, $5)
591
+ VALUES ${rows.join(", ")}
421
592
  ON CONFLICT (id) DO UPDATE SET
422
593
  text = EXCLUDED.text,
423
594
  source = EXCLUDED.source,
424
595
  metadata = EXCLUDED.metadata,
425
- embedding = EXCLUDED.embedding`, [
426
- id,
427
- text,
428
- source ?? null,
429
- metadata !== void 0 ? JSON.stringify(metadata) : null,
430
- toVectorLiteral(record.embedding)
431
- ]);
596
+ embedding = EXCLUDED.embedding`, params);
432
597
  }
433
598
  }
434
599
  async remove(documentId) {
@@ -464,11 +629,12 @@ var PgVectorStore = class {
464
629
  return rows.length;
465
630
  }
466
631
  async listDocuments(filter) {
467
- const where = buildWhere(filter, 1);
632
+ const params = [];
633
+ const where = this.where(filter, params);
468
634
  const rows = await this.client.query(`SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata
469
635
  FROM ${this.table}
470
- ${where.sql}
471
- ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, where.params);
636
+ ${where}
637
+ ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, params);
472
638
  return rows.map((row) => ({
473
639
  id: row.doc_id,
474
640
  ...row.metadata !== null ? {
@@ -519,11 +685,12 @@ var PgVectorStore = class {
519
685
  if (filterMatchesNothing(filter)) {
520
686
  return [];
521
687
  }
522
- const where = buildWhere(filter, 1);
688
+ const params = [];
689
+ const where = this.where(filter, params);
523
690
  const rows = await this.client.query(`SELECT DISTINCT ${DOCUMENT_ID_FROM_CHUNK} AS doc_id
524
691
  FROM ${this.table}
525
- ${where.sql}
526
- ORDER BY doc_id`, where.params);
692
+ ${where}
693
+ ORDER BY doc_id`, params);
527
694
  return rows.map((row) => row.doc_id);
528
695
  }
529
696
  /** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
@@ -547,81 +714,207 @@ var PgVectorStore = class {
547
714
  if (filterMatchesNothing(filter)) {
548
715
  return 0;
549
716
  }
550
- const where = buildWhere(filter, 1);
551
- const rows = await this.client.query(`DELETE FROM ${this.table} ${where.sql} RETURNING id`, where.params);
717
+ const params = [];
718
+ const where = this.where(filter, params);
719
+ const rows = await this.client.query(`DELETE FROM ${this.table} ${where} RETURNING id`, params);
552
720
  return rows.length;
553
721
  }
554
722
  async countChunks(filter) {
555
723
  if (filterMatchesNothing(filter)) {
556
724
  return 0;
557
725
  }
558
- const where = buildWhere(filter, 1);
559
- const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where.sql}`, where.params);
726
+ const params = [];
727
+ const where = this.where(filter, params);
728
+ const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where}`, params);
560
729
  return Number(rows[0]?.count ?? 0);
561
730
  }
731
+ /**
732
+ * Cosine similarity, nearest first. On a mixed-dimension table only vectors as wide as the query
733
+ * are compared (through that width's partial index when it has one); chunks without an embedding
734
+ * are never returned.
735
+ */
562
736
  async search(embedding, options) {
563
- const vector = toVectorLiteral(embedding);
564
- const where = buildWhere(options.filter, 3);
565
- const rows = await this.client.query(`SELECT id, text, source, metadata, 1 - (embedding <=> $1::vector) AS score
737
+ if (embedding.length === 0) {
738
+ return [];
739
+ }
740
+ const params = [
741
+ toVectorLiteral(embedding),
742
+ options.topK
743
+ ];
744
+ const conditions = this.whereConditions(options.filter, params);
745
+ let vectorType = "vector";
746
+ if (this.dimensions === void 0) {
747
+ const width = embedding.length;
748
+ if (this.indexedDimensions.includes(width)) {
749
+ vectorType = `vector(${width})`;
750
+ }
751
+ conditions.push("embedding IS NOT NULL", `vector_dims(embedding) = ${width}`);
752
+ } else if (this.nullableEmbeddings) {
753
+ conditions.push("embedding IS NOT NULL");
754
+ }
755
+ const column = vectorType === "vector" ? "embedding" : `(embedding::${vectorType})`;
756
+ const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score
566
757
  FROM ${this.table}
567
- ${where.sql}
568
- ORDER BY embedding <=> $1::vector
569
- LIMIT $2`, [
570
- vector,
571
- options.topK,
572
- ...where.params
573
- ]);
574
- return rows.map((row) => ({
575
- id: row.id,
576
- text: row.text,
577
- score: Number(row.score),
578
- ...row.source !== null ? {
579
- source: row.source
580
- } : {},
581
- ...row.metadata !== null ? {
582
- metadata: row.metadata
583
- } : {}
584
- }));
758
+ ${conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""}
759
+ ORDER BY ${column} <=> $1::${vectorType}
760
+ LIMIT $2`;
761
+ const rows = await this.withSearchSettings((client) => client.query(sql, params));
762
+ return rows.map(toPassage);
763
+ }
764
+ /**
765
+ * The SQL conditions (ANDed) a metadata `filter` compiles to, appending their bindings to `params`
766
+ * (placeholders are numbered after what `params` already holds). Every statement that takes a
767
+ * filter goes through here, so a subclass that maps the filter onto its own columns — a tenant id,
768
+ * an access list — overrides this one method. Return `['false']` to deny.
769
+ */
770
+ whereConditions(filter, params) {
771
+ return buildWhere(filter, params);
772
+ }
773
+ where(filter, params) {
774
+ const conditions = this.whereConditions(filter, params);
775
+ return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
776
+ }
777
+ /** Run a read with the configured HNSW settings `SET LOCAL` in one transaction, when possible. */
778
+ async withSearchSettings(read) {
779
+ const transaction = this.client.transaction?.bind(this.client);
780
+ if (transaction === void 0) {
781
+ return read(this.client);
782
+ }
783
+ const iterative = this.iterativeScan !== void 0 && await this.supportsIterativeScan();
784
+ if (!iterative && this.efSearch === void 0) {
785
+ return read(this.client);
786
+ }
787
+ return transaction(async (tx) => {
788
+ if (iterative) {
789
+ await tx.query(`SELECT set_config('hnsw.iterative_scan', $1, true)`, [
790
+ this.iterativeScan
791
+ ]);
792
+ }
793
+ if (this.efSearch !== void 0) {
794
+ await tx.query(`SELECT set_config('hnsw.ef_search', $1, true)`, [
795
+ String(this.efSearch)
796
+ ]);
797
+ }
798
+ return read(tx);
799
+ });
800
+ }
801
+ /** `hnsw.iterative_scan` exists from pgvector 0.8.0; setting it on an older one is an error. */
802
+ supportsIterativeScan() {
803
+ this.iterativeScanSupport ??= this.client.query(`SELECT extversion FROM pg_extension WHERE extname = 'vector'`).then((rows) => {
804
+ const [major = 0, minor = 0] = (rows[0]?.extversion ?? "0.0").split(".").map(Number);
805
+ return major > 0 || minor >= 8;
806
+ }).catch(() => false);
807
+ return this.iterativeScanSupport;
585
808
  }
586
809
  };
587
- function buildWhere(filter, startIndex) {
810
+ var PgLexicalVectorStore = class extends PgVectorStore {
811
+ static {
812
+ __name(this, "PgLexicalVectorStore");
813
+ }
814
+ tsConfig;
815
+ tsColumn;
816
+ anyTermFallback;
817
+ constructor(client, options = {}) {
818
+ super(client, options);
819
+ const config = options.fullText?.config ?? "simple";
820
+ if (!SQL_IDENTIFIER.test(config)) {
821
+ throw new Error(`PgLexicalVectorStore: invalid text search configuration "${config}"`);
822
+ }
823
+ const column = options.fullText?.column;
824
+ if (column !== void 0 && !SQL_IDENTIFIER.test(column)) {
825
+ throw new Error(`PgLexicalVectorStore: invalid tsvector column "${column}"`);
826
+ }
827
+ this.tsConfig = config;
828
+ this.tsColumn = column;
829
+ this.anyTermFallback = options.fullText?.anyTermFallback ?? true;
830
+ }
831
+ schemaStatements() {
832
+ const statements = super.schemaStatements();
833
+ if (this.tsColumn === void 0) {
834
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_text_tsv_idx
835
+ ON ${this.table} USING gin (${this.tsvector()})`);
836
+ }
837
+ return statements;
838
+ }
839
+ async searchText(query, options) {
840
+ if (filterMatchesNothing(options.filter) || query.trim() === "") {
841
+ return [];
842
+ }
843
+ const all = await this.runTextSearch(`websearch_to_tsquery('${this.tsConfig}', $1)`, query, options);
844
+ if (all.length > 0 || !this.anyTermFallback) {
845
+ return all;
846
+ }
847
+ const terms = [
848
+ ...new Set(query.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [])
849
+ ].slice(0, 24);
850
+ if (terms.length < 2) {
851
+ return all;
852
+ }
853
+ return this.runTextSearch(`to_tsquery('${this.tsConfig}', $1)`, terms.map((term) => `'${term}'`).join(" | "), options);
854
+ }
855
+ async runTextSearch(tsquery, query, options) {
856
+ const params = [
857
+ query,
858
+ options.topK
859
+ ];
860
+ const conditions = [
861
+ `${this.tsvector()} @@ ${tsquery}`,
862
+ ...this.whereConditions(options.filter, params)
863
+ ];
864
+ const rows = await this.client.query(`SELECT id, text, source, metadata, ts_rank_cd(${this.tsvector()}, ${tsquery}) AS score
865
+ FROM ${this.table}
866
+ WHERE ${conditions.join(" AND ")}
867
+ ORDER BY score DESC, id
868
+ LIMIT $2`, params);
869
+ return rows.map(toPassage);
870
+ }
871
+ /** Must be character-for-character the index expression, or the planner won't use the index. */
872
+ tsvector() {
873
+ return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;
874
+ }
875
+ };
876
+ var SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
877
+ function toPassage(row) {
878
+ return {
879
+ id: row.id,
880
+ text: row.text,
881
+ score: Number(row.score),
882
+ ...row.source !== null ? {
883
+ source: row.source
884
+ } : {},
885
+ ...row.metadata !== null ? {
886
+ metadata: row.metadata
887
+ } : {}
888
+ };
889
+ }
890
+ __name(toPassage, "toPassage");
891
+ function buildWhere(filter, params) {
588
892
  if (filter === void 0 || Object.keys(filter).length === 0) {
589
- return {
590
- sql: "",
591
- params: []
592
- };
893
+ return [];
593
894
  }
594
895
  const stripped = stripNulBytes(filter);
595
896
  const clauses = [];
596
- const params = [];
597
897
  const scalar = {};
598
- let index = startIndex;
599
898
  for (const [key, value] of Object.entries(stripped)) {
600
899
  if (Array.isArray(value)) {
601
900
  if (value.length === 0) {
602
901
  clauses.push("false");
603
902
  continue;
604
903
  }
605
- const keyParam = `$${index}`;
606
904
  params.push(key);
607
- index += 1;
608
- const arrParam = `$${index}`;
905
+ const keyParam = `$${params.length}`;
609
906
  params.push(value.map(String));
610
- index += 1;
907
+ const arrParam = `$${params.length}`;
611
908
  clauses.push(`(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`);
612
909
  } else {
613
910
  scalar[key] = value;
614
911
  }
615
912
  }
616
913
  if (Object.keys(scalar).length > 0) {
617
- clauses.push(`metadata @> $${index}::jsonb`);
618
914
  params.push(JSON.stringify(scalar));
619
- index += 1;
915
+ clauses.push(`metadata @> $${params.length}::jsonb`);
620
916
  }
621
- return {
622
- sql: `WHERE ${clauses.join(" AND ")}`,
623
- params
624
- };
917
+ return clauses;
625
918
  }
626
919
  __name(buildWhere, "buildWhere");
627
920
  var DOCUMENT_ID_FROM_CHUNK = "regexp_replace(id, '#[0-9]+$', '')";
@@ -1431,7 +1724,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1431
1724
  attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);
1432
1725
  }
1433
1726
  const inlineScore = scoreSource === "lexical" ? toStr(reply[index + 1]) : void 0;
1434
- passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1727
+ passages.push(toPassage2(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1435
1728
  }
1436
1729
  }
1437
1730
  return passages;
@@ -1439,7 +1732,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1439
1732
  if (typeof reply === "object" && reply !== null && "results" in reply) {
1440
1733
  const results = reply.results;
1441
1734
  if (Array.isArray(results)) {
1442
- return results.map((result) => toPassage(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1735
+ return results.map((result) => toPassage2(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1443
1736
  }
1444
1737
  }
1445
1738
  return [];
@@ -1472,7 +1765,7 @@ function readAttributes(result) {
1472
1765
  return attrs;
1473
1766
  }
1474
1767
  __name(readAttributes, "readAttributes");
1475
- function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1768
+ function toPassage2(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1476
1769
  const score = scoreSource === "lexical" ? Number(inlineScore ?? "0") : 1 - Number(attrs.vector_score ?? "1");
1477
1770
  const source = attrs.source;
1478
1771
  return {
@@ -1485,7 +1778,7 @@ function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1485
1778
  ...parseMetadata(attrs.metadata_json)
1486
1779
  };
1487
1780
  }
1488
- __name(toPassage, "toPassage");
1781
+ __name(toPassage2, "toPassage");
1489
1782
  function parseMetadata(raw) {
1490
1783
  if (raw === void 0) {
1491
1784
  return {};
@@ -1878,7 +2171,7 @@ async function ingestDocuments(documents, options) {
1878
2171
  __name(ingestDocuments, "ingestDocuments");
1879
2172
 
1880
2173
  // src/retrieval-telemetry.ts
1881
- import { AsyncLocalStorage } from "node:async_hooks";
2174
+ import { AsyncLocalStorage } from "async_hooks";
1882
2175
  import { channelName, emit, getChannel } from "@dudousxd/nestjs-diagnostics";
1883
2176
  var RAG_DIAGNOSTIC_LIB = "rag";
1884
2177
  var RAG_RETRIEVAL_EVENT = "retrieval";
@@ -2030,13 +2323,180 @@ function createRetrievalTool(retriever, options = {}) {
2030
2323
  };
2031
2324
  }
2032
2325
  __name(createRetrievalTool, "createRetrievalTool");
2326
+
2327
+ // src/openai-embeddings.ts
2328
+ var HttpModelError = class extends Error {
2329
+ static {
2330
+ __name(this, "HttpModelError");
2331
+ }
2332
+ status;
2333
+ constructor(status, message) {
2334
+ super(message), this.status = status;
2335
+ this.name = "HttpModelError";
2336
+ }
2337
+ };
2338
+ function openAiEmbeddings(options) {
2339
+ const url = `${(options.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/embeddings`;
2340
+ const batchSize = Math.max(1, Math.floor(options.batchSize ?? 64));
2341
+ const maxInputChars = options.maxInputChars ?? 24e3;
2342
+ const timeoutMs = options.timeoutMs ?? 12e4;
2343
+ const doFetch = options.fetch ?? fetch;
2344
+ return {
2345
+ async embed(texts) {
2346
+ const vectors = [];
2347
+ for (let start = 0; start < texts.length; start += batchSize) {
2348
+ const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
2349
+ const response = await doFetch(url, {
2350
+ method: "POST",
2351
+ headers: {
2352
+ "content-type": "application/json",
2353
+ ...options.apiKey ? {
2354
+ authorization: `Bearer ${options.apiKey}`
2355
+ } : {},
2356
+ ...options.headers
2357
+ },
2358
+ body: JSON.stringify({
2359
+ model: options.model,
2360
+ input,
2361
+ ...options.dimensions !== void 0 ? {
2362
+ dimensions: options.dimensions
2363
+ } : {}
2364
+ }),
2365
+ signal: AbortSignal.timeout(timeoutMs)
2366
+ });
2367
+ const body = await response.text();
2368
+ if (!response.ok) {
2369
+ throw new HttpModelError(response.status, `Embeddings request failed (${response.status}): ${errorMessageOf(body)}`);
2370
+ }
2371
+ const json = parseJson(body);
2372
+ const items = [
2373
+ ...json?.data ?? []
2374
+ ].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
2375
+ if (items.length !== input.length || !items.every((item) => Array.isArray(item.embedding))) {
2376
+ throw new HttpModelError(502, `Embeddings returned ${items.length} vectors for ${input.length} inputs`);
2377
+ }
2378
+ for (const item of items) {
2379
+ vectors.push(item.embedding);
2380
+ }
2381
+ const tokens = json?.usage?.total_tokens ?? json?.usage?.prompt_tokens;
2382
+ if (typeof tokens === "number" && Number.isFinite(tokens)) {
2383
+ options.onUsage?.(tokens);
2384
+ }
2385
+ }
2386
+ return vectors;
2387
+ }
2388
+ };
2389
+ }
2390
+ __name(openAiEmbeddings, "openAiEmbeddings");
2391
+ function parseJson(body) {
2392
+ try {
2393
+ return JSON.parse(body);
2394
+ } catch {
2395
+ return null;
2396
+ }
2397
+ }
2398
+ __name(parseJson, "parseJson");
2399
+ function errorMessageOf(body) {
2400
+ const json = parseJson(body);
2401
+ const message = typeof json?.error === "string" ? json.error : json?.error?.message ?? json?.message ?? void 0;
2402
+ return typeof message === "string" ? message : body.slice(0, 300);
2403
+ }
2404
+ __name(errorMessageOf, "errorMessageOf");
2405
+
2406
+ // src/http-reranker.ts
2407
+ var HttpReranker = class {
2408
+ static {
2409
+ __name(this, "HttpReranker");
2410
+ }
2411
+ options;
2412
+ format;
2413
+ doFetch;
2414
+ constructor(options) {
2415
+ this.options = options;
2416
+ this.format = options.format ?? "auto";
2417
+ this.doFetch = options.fetch ?? fetch;
2418
+ }
2419
+ async rerank(query, passages, options = {}) {
2420
+ if (passages.length === 0) {
2421
+ return [];
2422
+ }
2423
+ const topK = options.topK ?? passages.length;
2424
+ const documents = passages.map((passage) => passage.text);
2425
+ const response = await this.doFetch(this.options.url, {
2426
+ method: "POST",
2427
+ headers: {
2428
+ "content-type": "application/json",
2429
+ ...this.options.apiKey ? {
2430
+ authorization: `Bearer ${this.options.apiKey}`
2431
+ } : {},
2432
+ ...this.options.headers
2433
+ },
2434
+ body: JSON.stringify({
2435
+ ...this.options.model !== void 0 ? {
2436
+ model: this.options.model
2437
+ } : {},
2438
+ query,
2439
+ ...this.format !== "tei" ? {
2440
+ documents,
2441
+ top_n: topK
2442
+ } : {},
2443
+ ...this.format !== "cohere" ? {
2444
+ texts: documents
2445
+ } : {}
2446
+ }),
2447
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
2448
+ });
2449
+ const body = await response.text();
2450
+ if (!response.ok) {
2451
+ throw new HttpModelError(response.status, `Rerank request failed (${response.status}): ${errorMessageOf(body)}`);
2452
+ }
2453
+ const items = itemsOf(body);
2454
+ if (items === void 0) {
2455
+ throw new HttpModelError(502, "Rerank response has no results");
2456
+ }
2457
+ const seen = /* @__PURE__ */ new Set();
2458
+ const reranked = [];
2459
+ for (const item of items) {
2460
+ const index = item.index;
2461
+ const passage = typeof index === "number" ? passages[index] : void 0;
2462
+ if (passage === void 0 || seen.has(index)) {
2463
+ continue;
2464
+ }
2465
+ seen.add(index);
2466
+ reranked.push({
2467
+ ...passage,
2468
+ score: Number(item.relevance_score ?? item.score ?? 0)
2469
+ });
2470
+ }
2471
+ reranked.sort((a, b) => b.score - a.score);
2472
+ return reranked.slice(0, topK);
2473
+ }
2474
+ };
2475
+ function itemsOf(body) {
2476
+ let json;
2477
+ try {
2478
+ json = JSON.parse(body);
2479
+ } catch {
2480
+ return void 0;
2481
+ }
2482
+ if (Array.isArray(json)) {
2483
+ return json;
2484
+ }
2485
+ const wrapped = json;
2486
+ const items = wrapped?.results ?? wrapped?.data;
2487
+ return Array.isArray(items) ? items : void 0;
2488
+ }
2489
+ __name(itemsOf, "itemsOf");
2033
2490
  export {
2034
2491
  EmbeddingRetriever,
2035
2492
  FilteredRetriever,
2493
+ HttpModelError,
2494
+ HttpReranker,
2036
2495
  HybridRetriever,
2037
2496
  KeywordRetriever,
2038
2497
  LexicalRetriever,
2039
2498
  MemoryVectorStore,
2499
+ PgLexicalVectorStore,
2040
2500
  PgVectorStore,
2041
2501
  RAG_DIAGNOSTIC_LIB,
2042
2502
  RAG_RETRIEVAL_CHANNEL,
@@ -2059,6 +2519,8 @@ export {
2059
2519
  ingestChunks,
2060
2520
  ingestDocuments,
2061
2521
  instrumentRetriever,
2062
- isLexicalVectorStore
2522
+ isLexicalVectorStore,
2523
+ openAiEmbeddings,
2524
+ toPgClient
2063
2525
  };
2064
2526
  //# sourceMappingURL=index.js.map