@dudousxd/nestjs-agent-rag 0.10.3 → 0.12.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
@@ -21,12 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ DEFAULT_STOP_WORDS: () => DEFAULT_STOP_WORDS,
24
25
  EmbeddingRetriever: () => EmbeddingRetriever,
25
26
  FilteredRetriever: () => FilteredRetriever,
27
+ HttpModelError: () => HttpModelError,
28
+ HttpReranker: () => HttpReranker,
26
29
  HybridRetriever: () => HybridRetriever,
27
30
  KeywordRetriever: () => KeywordRetriever,
28
31
  LexicalRetriever: () => LexicalRetriever,
29
32
  MemoryVectorStore: () => MemoryVectorStore,
33
+ PgLexicalVectorStore: () => PgLexicalVectorStore,
30
34
  PgVectorStore: () => PgVectorStore,
31
35
  RAG_DIAGNOSTIC_LIB: () => RAG_DIAGNOSTIC_LIB,
32
36
  RAG_RETRIEVAL_CHANNEL: () => RAG_RETRIEVAL_CHANNEL,
@@ -35,6 +39,7 @@ __export(index_exports, {
35
39
  RedisVectorStore: () => RedisVectorStore,
36
40
  RerankingRetriever: () => RerankingRetriever,
37
41
  UnsafeRemovalError: () => UnsafeRemovalError,
42
+ anyTermTsquery: () => anyTermTsquery,
38
43
  applyMetadataPatch: () => applyMetadataPatch,
39
44
  chunkDocuments: () => chunkDocuments,
40
45
  chunkIndexOf: () => chunkIndexOf,
@@ -46,10 +51,14 @@ __export(index_exports, {
46
51
  describesRetrieval: () => describesRetrieval,
47
52
  documentIdOf: () => documentIdOf,
48
53
  emitRagRetrieval: () => emitRagRetrieval,
54
+ hasSearchSyntax: () => hasSearchSyntax,
49
55
  ingestChunks: () => ingestChunks,
50
56
  ingestDocuments: () => ingestDocuments,
51
57
  instrumentRetriever: () => instrumentRetriever,
52
- isLexicalVectorStore: () => isLexicalVectorStore
58
+ isLexicalVectorStore: () => isLexicalVectorStore,
59
+ keywordTerms: () => keywordTerms,
60
+ openAiEmbeddings: () => openAiEmbeddings,
61
+ toPgClient: () => toPgClient
53
62
  });
54
63
  module.exports = __toCommonJS(index_exports);
55
64
 
@@ -408,6 +417,181 @@ function cosineSimilarity(a, b) {
408
417
  }
409
418
  __name(cosineSimilarity, "cosineSimilarity");
410
419
 
420
+ // src/lexical-query.ts
421
+ var words = /* @__PURE__ */ __name((s) => new Set(s.split(/\s+/).filter(Boolean)), "words");
422
+ var DEFAULT_STOP_WORDS = {
423
+ english: words(`
424
+ me my myself we our ours ourselves you your yours yourself yourselves he him his himself she
425
+ her hers herself it its itself they them their theirs themselves what which who whom this that
426
+ these those am is are was were be been being have has had having do does did doing an the and
427
+ but if or because as until while of at by for with about against between into through during
428
+ before after above below to from up down in out on off over under again further then once here
429
+ there when where why how all any both each few more most other some such no nor not only own
430
+ same so than too very can will just don should now`),
431
+ portuguese: words(`
432
+ de que do da em um para com n\xE3o uma os no se na por mais as dos como mas ao ele das seu sua ou
433
+ quando muito nos j\xE1 eu tamb\xE9m s\xF3 pelo pela at\xE9 isso ela entre depois sem mesmo aos seus quem
434
+ nas me esse eles voc\xEA essa num nem suas meu \xE0s minha numa pelos elas qual n\xF3s lhe deles essas
435
+ esses pelas este dele tu te voc\xEAs vos lhes meus minhas teu tua teus tuas nosso nossa nossos
436
+ nossas dela delas esta estes estas aquele aquela aqueles aquelas isto aquilo estou est\xE1 estamos
437
+ est\xE3o estive esteve estivemos estiveram estava est\xE1vamos estavam estivera estiv\xE9ramos esteja
438
+ estejamos estejam estivesse estiv\xE9ssemos estivessem estiver estivermos estiverem hei h\xE1 havemos
439
+ h\xE3o houve houvemos houveram houvera houv\xE9ramos haja hajamos hajam houvesse houv\xE9ssemos
440
+ houvessem houver houvermos houverem houverei houver\xE1 houveremos houver\xE3o houveria houver\xEDamos
441
+ houveriam sou somos s\xE3o era \xE9ramos eram fui foi fomos foram fora f\xF4ramos seja sejamos sejam
442
+ fosse f\xF4ssemos fossem for formos forem serei ser\xE1 seremos ser\xE3o seria ser\xEDamos seriam tenho tem
443
+ temos t\xE9m tinha t\xEDnhamos tinham tive teve tivemos tiveram tivera tiv\xE9ramos tenha tenhamos
444
+ tenham tivesse tiv\xE9ssemos tivessem tiver tivermos tiverem terei ter\xE1 teremos ter\xE3o teria
445
+ ter\xEDamos teriam`),
446
+ spanish: words(`
447
+ de la que el en los del se las por un para con no una su al lo como m\xE1s pero sus le ya este s\xED
448
+ porque esta entre cuando muy sin sobre tambi\xE9n me hasta hay donde quien desde todo nos durante
449
+ todos uno les ni contra otros ese eso ante ellos esto m\xED antes algunos qu\xE9 unos yo otro otras
450
+ otra \xE9l tanto esa estos mucho quienes nada muchos cual poco ella estar estas algunas algo
451
+ nosotros mi mis t\xFA te ti tu tus ellas nosotras vosotros vosotras os m\xEDo m\xEDa m\xEDos m\xEDas tuyo
452
+ tuya tuyos tuyas suyo suya suyos suyas nuestro nuestra nuestros nuestras vuestro vuestra
453
+ vuestros vuestras esos esas estoy est\xE1s est\xE1 estamos est\xE1is est\xE1n est\xE9 est\xE9s estemos est\xE9is
454
+ est\xE9n estar\xE9 estar\xE1s estar\xE1 estaremos estar\xE9is estar\xE1n estar\xEDa estar\xEDas estar\xEDamos estar\xEDais
455
+ estar\xEDan estaba estabas est\xE1bamos estabais estaban estuve estuviste estuvo estuvimos
456
+ estuvisteis estuvieron estuviera estuvieras estuvi\xE9ramos estuvierais estuvieran estuviese
457
+ estuvieses estuvi\xE9semos estuvieseis estuviesen estando estad he has ha hemos hab\xE9is han haya
458
+ hayas hayamos hay\xE1is hayan habr\xE9 habr\xE1s habr\xE1 habremos habr\xE9is habr\xE1n habr\xEDa habr\xEDas habr\xEDamos
459
+ habr\xEDais habr\xEDan hab\xEDa hab\xEDas hab\xEDamos hab\xEDais hab\xEDan hube hubiste hubo hubimos hubisteis
460
+ hubieron hubiera hubieras hubi\xE9ramos hubierais hubieran hubiese hubieses hubi\xE9semos hubieseis
461
+ hubiesen habiendo habido habida habidos habidas soy eres es somos sois son sea seas seamos
462
+ se\xE1is sean ser\xE9 ser\xE1s ser\xE1 seremos ser\xE9is ser\xE1n ser\xEDa ser\xEDas ser\xEDamos ser\xEDais ser\xEDan era eras
463
+ \xE9ramos erais eran fui fuiste fue fuimos fuisteis fueron fuera fueras fu\xE9ramos fuerais fueran
464
+ fuese fueses fu\xE9semos fueseis fuesen tengo tienes tiene tenemos ten\xE9is tienen tenga tengas
465
+ tengamos teng\xE1is tengan tendr\xE9 tendr\xE1s tendr\xE1 tendremos tendr\xE9is tendr\xE1n tendr\xEDa tendr\xEDas
466
+ tendr\xEDamos tendr\xEDais tendr\xEDan ten\xEDa ten\xEDas ten\xEDamos ten\xEDais ten\xEDan tuve tuviste tuvo tuvimos
467
+ tuvisteis tuvieron tuviera tuvieras tuvi\xE9ramos tuvierais tuvieran tuviese tuvieses tuvi\xE9semos
468
+ tuvieseis tuviesen teniendo tenido tenida tenidos tenidas tened`)
469
+ };
470
+ var TOKEN = /[\p{L}\p{N}]{2,}/gu;
471
+ function keywordTerms(query, max = 24, stopWords = DEFAULT_STOP_WORDS) {
472
+ const tokens = [
473
+ ...new Set(query.toLowerCase().normalize("NFC").match(TOKEN) ?? [])
474
+ ];
475
+ const counts = Object.entries(stopWords).map(([, stop2]) => [
476
+ stop2,
477
+ tokens.filter((t) => stop2.has(t)).length
478
+ ]);
479
+ const best = Math.max(0, ...counts.map(([, n]) => n));
480
+ if (best === 0) return tokens.slice(0, max);
481
+ const stop = counts.filter(([, n]) => n === best).map(([s]) => s);
482
+ const terms = tokens.filter((t) => !stop.some((s) => s.has(t)));
483
+ return (terms.length > 0 ? terms : tokens).slice(0, max);
484
+ }
485
+ __name(keywordTerms, "keywordTerms");
486
+ function hasSearchSyntax(query) {
487
+ return /"[^"]+"/.test(query) || /(^|\s)-[\p{L}\p{N}]/u.test(query);
488
+ }
489
+ __name(hasSearchSyntax, "hasSearchSyntax");
490
+ function anyTermTsquery(terms) {
491
+ return terms.map((t) => `'${t.replace(/'/g, "")}'`).join(" | ");
492
+ }
493
+ __name(anyTermTsquery, "anyTermTsquery");
494
+
495
+ // src/pg-client.ts
496
+ var ADAPTED = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/pg-client");
497
+ function toPgClient(source) {
498
+ if (source[ADAPTED]) {
499
+ return source;
500
+ }
501
+ if (isDrizzle(source)) {
502
+ return toPgClient(source.$client);
503
+ }
504
+ if (typeof source.unsafe === "function") {
505
+ return fromPostgresJs(source);
506
+ }
507
+ return fromQueryable(source);
508
+ }
509
+ __name(toPgClient, "toPgClient");
510
+ function isDrizzle(source) {
511
+ const client = source.$client;
512
+ return client !== null && client !== void 0 && typeof client === "object";
513
+ }
514
+ __name(isDrizzle, "isDrizzle");
515
+ function mark(client) {
516
+ Object.defineProperty(client, ADAPTED, {
517
+ value: true
518
+ });
519
+ return client;
520
+ }
521
+ __name(mark, "mark");
522
+ function rowsOf(result) {
523
+ if (Array.isArray(result)) {
524
+ return result;
525
+ }
526
+ const rows = result?.rows;
527
+ return Array.isArray(rows) ? rows : [];
528
+ }
529
+ __name(rowsOf, "rowsOf");
530
+ function fromPostgresJs(sql) {
531
+ const client = {
532
+ query: /* @__PURE__ */ __name(async (text, params = []) => (
533
+ // postgres.js resolves a RowList (an Array subclass); copy it to a plain array.
534
+ [
535
+ ...rowsOf(await sql.unsafe(text, params))
536
+ ]
537
+ ), "query")
538
+ };
539
+ if (typeof sql.begin === "function") {
540
+ const begin = sql.begin.bind(sql);
541
+ client.transaction = (work) => Promise.resolve(begin((tx) => work(fromPostgresJs(tx))));
542
+ }
543
+ return mark(client);
544
+ }
545
+ __name(fromPostgresJs, "fromPostgresJs");
546
+ function fromQueryable(source) {
547
+ const client = {
548
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await source.query(text, params)), "query")
549
+ };
550
+ if (typeof source.transaction === "function") {
551
+ const transaction = source.transaction.bind(source);
552
+ client.transaction = (work) => transaction((tx) => work(toPgClient(tx)));
553
+ } else if (isNodePgPool(source)) {
554
+ client.transaction = async (work) => {
555
+ const connection = await source.connect();
556
+ let broken = false;
557
+ try {
558
+ return await runInTransaction(connection, work, () => {
559
+ broken = true;
560
+ });
561
+ } finally {
562
+ connection.release(broken ? new Error("ROLLBACK failed") : void 0);
563
+ }
564
+ };
565
+ } else if (isNodePgClient(source)) {
566
+ client.transaction = (work) => runInTransaction(source, work);
567
+ }
568
+ return mark(client);
569
+ }
570
+ __name(fromQueryable, "fromQueryable");
571
+ async function runInTransaction(connection, work, onRollbackFailed) {
572
+ const tx = mark({
573
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await connection.query(text, params)), "query")
574
+ });
575
+ await connection.query("BEGIN");
576
+ try {
577
+ const result = await work(tx);
578
+ await connection.query("COMMIT");
579
+ return result;
580
+ } catch (error) {
581
+ await connection.query("ROLLBACK").catch(() => onRollbackFailed?.());
582
+ throw error;
583
+ }
584
+ }
585
+ __name(runInTransaction, "runInTransaction");
586
+ function isNodePgPool(source) {
587
+ return typeof source.connect === "function" && typeof source.totalCount === "number";
588
+ }
589
+ __name(isNodePgPool, "isNodePgPool");
590
+ function isNodePgClient(source) {
591
+ return typeof source.release === "function" || typeof source.processID === "number" || source.connectionParameters !== void 0;
592
+ }
593
+ __name(isNodePgClient, "isNodePgClient");
594
+
411
595
  // src/pg-vector-store.ts
412
596
  var NUL_BYTE = String.fromCharCode(0);
413
597
  function stripNulBytes(value) {
@@ -436,11 +620,37 @@ var PgVectorStore = class {
436
620
  }
437
621
  client;
438
622
  table;
623
+ /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */
439
624
  dimensions;
625
+ /** The widths with a partial HNSW index, for a mixed-dimension table. */
626
+ indexedDimensions;
627
+ nullableEmbeddings;
628
+ iterativeScan;
629
+ efSearch;
630
+ upsertBatchSize;
631
+ iterativeScanSupport;
440
632
  constructor(client, options = {}) {
441
- this.client = client;
633
+ this.client = toPgClient(client);
442
634
  this.table = options.table ?? "agent_rag_chunks";
443
- this.dimensions = options.dimensions ?? 1536;
635
+ const dimensions = options.dimensions ?? 1536;
636
+ if (typeof dimensions === "number") {
637
+ this.dimensions = dimensions;
638
+ this.indexedDimensions = [];
639
+ } else {
640
+ for (const width of dimensions) {
641
+ if (!Number.isInteger(width) || width <= 0) {
642
+ throw new Error(`PgVectorStore: invalid embedding width ${String(width)}`);
643
+ }
644
+ }
645
+ this.dimensions = void 0;
646
+ this.indexedDimensions = [
647
+ ...new Set(dimensions)
648
+ ];
649
+ }
650
+ this.nullableEmbeddings = options.nullableEmbeddings ?? false;
651
+ this.iterativeScan = options.iterativeScan;
652
+ this.efSearch = options.efSearch;
653
+ this.upsertBatchSize = Math.max(1, Math.floor(options.upsertBatchSize ?? 100));
444
654
  }
445
655
  /** Telemetry self-description — the table is the namespace a retrieval was served from. */
446
656
  describeRetrieval() {
@@ -449,38 +659,77 @@ var PgVectorStore = class {
449
659
  collection: this.table
450
660
  };
451
661
  }
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} (
662
+ /**
663
+ * The idempotent DDL {@link PgVectorStore.ensureSchema} runs, in order — for a migration instead
664
+ * of a boot-time side effect. On a populated table, run the index statements `CONCURRENTLY` there.
665
+ */
666
+ schemaStatements() {
667
+ const mixed = this.dimensions === void 0;
668
+ const column = mixed ? "vector" : `vector(${this.dimensions})`;
669
+ const statements = [
670
+ "CREATE EXTENSION IF NOT EXISTS vector",
671
+ `CREATE TABLE IF NOT EXISTS ${this.table} (
456
672
  id TEXT PRIMARY KEY,
457
673
  text TEXT NOT NULL,
458
674
  source TEXT,
459
675
  metadata JSONB,
460
- embedding vector(${this.dimensions}) NOT NULL
461
- )`);
462
- await this.client.query(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
676
+ embedding ${column}${this.nullableEmbeddings ? "" : " NOT NULL"}
677
+ )`
678
+ ];
679
+ if (mixed) {
680
+ for (const width of this.indexedDimensions) {
681
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_${width}_idx
682
+ ON ${this.table} USING hnsw ((embedding::vector(${width})) vector_cosine_ops)
683
+ WHERE vector_dims(embedding) = ${width}`);
684
+ }
685
+ } else {
686
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
463
687
  ON ${this.table} USING hnsw (embedding vector_cosine_ops)`);
688
+ }
689
+ return statements;
690
+ }
691
+ /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index(es). */
692
+ async ensureSchema() {
693
+ for (const statement of this.schemaStatements()) {
694
+ await this.client.query(statement);
695
+ }
464
696
  }
697
+ /**
698
+ * Multi-row `INSERT … ON CONFLICT (id) DO UPDATE`, {@link PgVectorStoreOptions.upsertBatchSize}
699
+ * rows per statement. An empty `embedding` is written as `NULL` (see
700
+ * {@link PgVectorStoreOptions.nullableEmbeddings}).
701
+ */
465
702
  async upsert(records) {
703
+ const byId = /* @__PURE__ */ new Map();
466
704
  for (const record of records) {
467
705
  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;
706
+ byId.delete(id);
707
+ byId.set(id, {
708
+ ...record,
709
+ id
710
+ });
711
+ }
712
+ const unique = [
713
+ ...byId.values()
714
+ ];
715
+ for (let start = 0; start < unique.length; start += this.upsertBatchSize) {
716
+ const batch = unique.slice(start, start + this.upsertBatchSize);
717
+ const params = [];
718
+ const rows = batch.map((record) => {
719
+ const text = stripNulBytes(record.text);
720
+ const source = record.source !== void 0 ? stripNulBytes(record.source) : void 0;
721
+ const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
722
+ const base = params.length;
723
+ params.push(record.id, text, source ?? null, metadata !== void 0 ? JSON.stringify(metadata) : null, record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null);
724
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::vector)`;
725
+ });
471
726
  await this.client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
472
- VALUES ($1, $2, $3, $4, $5)
727
+ VALUES ${rows.join(", ")}
473
728
  ON CONFLICT (id) DO UPDATE SET
474
729
  text = EXCLUDED.text,
475
730
  source = EXCLUDED.source,
476
731
  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
- ]);
732
+ embedding = EXCLUDED.embedding`, params);
484
733
  }
485
734
  }
486
735
  async remove(documentId) {
@@ -516,11 +765,12 @@ var PgVectorStore = class {
516
765
  return rows.length;
517
766
  }
518
767
  async listDocuments(filter) {
519
- const where = buildWhere(filter, 1);
768
+ const params = [];
769
+ const where = this.where(filter, params);
520
770
  const rows = await this.client.query(`SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata
521
771
  FROM ${this.table}
522
- ${where.sql}
523
- ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, where.params);
772
+ ${where}
773
+ ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, params);
524
774
  return rows.map((row) => ({
525
775
  id: row.doc_id,
526
776
  ...row.metadata !== null ? {
@@ -571,11 +821,12 @@ var PgVectorStore = class {
571
821
  if (filterMatchesNothing(filter)) {
572
822
  return [];
573
823
  }
574
- const where = buildWhere(filter, 1);
824
+ const params = [];
825
+ const where = this.where(filter, params);
575
826
  const rows = await this.client.query(`SELECT DISTINCT ${DOCUMENT_ID_FROM_CHUNK} AS doc_id
576
827
  FROM ${this.table}
577
- ${where.sql}
578
- ORDER BY doc_id`, where.params);
828
+ ${where}
829
+ ORDER BY doc_id`, params);
579
830
  return rows.map((row) => row.doc_id);
580
831
  }
581
832
  /** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
@@ -599,81 +850,250 @@ var PgVectorStore = class {
599
850
  if (filterMatchesNothing(filter)) {
600
851
  return 0;
601
852
  }
602
- const where = buildWhere(filter, 1);
603
- const rows = await this.client.query(`DELETE FROM ${this.table} ${where.sql} RETURNING id`, where.params);
853
+ const params = [];
854
+ const where = this.where(filter, params);
855
+ const rows = await this.client.query(`DELETE FROM ${this.table} ${where} RETURNING id`, params);
604
856
  return rows.length;
605
857
  }
606
858
  async countChunks(filter) {
607
859
  if (filterMatchesNothing(filter)) {
608
860
  return 0;
609
861
  }
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);
862
+ const params = [];
863
+ const where = this.where(filter, params);
864
+ const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where}`, params);
612
865
  return Number(rows[0]?.count ?? 0);
613
866
  }
867
+ /**
868
+ * Cosine similarity, nearest first. On a mixed-dimension table only vectors as wide as the query
869
+ * are compared (through that width's partial index when it has one); chunks without an embedding
870
+ * are never returned.
871
+ */
614
872
  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
873
+ if (embedding.length === 0) {
874
+ return [];
875
+ }
876
+ const params = [
877
+ toVectorLiteral(embedding),
878
+ options.topK
879
+ ];
880
+ const conditions = this.whereConditions(options.filter, params);
881
+ let vectorType = "vector";
882
+ if (this.dimensions === void 0) {
883
+ const width = embedding.length;
884
+ if (this.indexedDimensions.includes(width)) {
885
+ vectorType = `vector(${width})`;
886
+ }
887
+ conditions.push("embedding IS NOT NULL", `vector_dims(embedding) = ${width}`);
888
+ } else if (this.nullableEmbeddings) {
889
+ conditions.push("embedding IS NOT NULL");
890
+ }
891
+ const column = vectorType === "vector" ? "embedding" : `(embedding::${vectorType})`;
892
+ const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score
618
893
  FROM ${this.table}
619
- ${where.sql}
620
- ORDER BY embedding <=> $1::vector
621
- LIMIT $2`, [
622
- vector,
894
+ ${conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""}
895
+ ORDER BY ${column} <=> $1::${vectorType}
896
+ LIMIT $2`;
897
+ const rows = await this.withSearchSettings((client) => client.query(sql, params));
898
+ return rows.map(toPassage);
899
+ }
900
+ /**
901
+ * The SQL conditions (ANDed) a metadata `filter` compiles to, appending their bindings to `params`
902
+ * (placeholders are numbered after what `params` already holds). Every statement that takes a
903
+ * filter goes through here, so a subclass that maps the filter onto its own columns — a tenant id,
904
+ * an access list — overrides this one method. Return `['false']` to deny.
905
+ */
906
+ whereConditions(filter, params) {
907
+ return buildWhere(filter, params);
908
+ }
909
+ where(filter, params) {
910
+ const conditions = this.whereConditions(filter, params);
911
+ return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
912
+ }
913
+ /** Run a read with the configured HNSW settings `SET LOCAL` in one transaction, when possible. */
914
+ async withSearchSettings(read) {
915
+ const transaction = this.client.transaction?.bind(this.client);
916
+ if (transaction === void 0) {
917
+ return read(this.client);
918
+ }
919
+ const iterative = this.iterativeScan !== void 0 && await this.supportsIterativeScan();
920
+ if (!iterative && this.efSearch === void 0) {
921
+ return read(this.client);
922
+ }
923
+ return transaction(async (tx) => {
924
+ if (iterative) {
925
+ await tx.query(`SELECT set_config('hnsw.iterative_scan', $1, true)`, [
926
+ this.iterativeScan
927
+ ]);
928
+ }
929
+ if (this.efSearch !== void 0) {
930
+ await tx.query(`SELECT set_config('hnsw.ef_search', $1, true)`, [
931
+ String(this.efSearch)
932
+ ]);
933
+ }
934
+ return read(tx);
935
+ });
936
+ }
937
+ /** `hnsw.iterative_scan` exists from pgvector 0.8.0; setting it on an older one is an error. */
938
+ supportsIterativeScan() {
939
+ this.iterativeScanSupport ??= this.client.query(`SELECT extversion FROM pg_extension WHERE extname = 'vector'`).then((rows) => {
940
+ const [major = 0, minor = 0] = (rows[0]?.extversion ?? "0.0").split(".").map(Number);
941
+ return major > 0 || minor >= 8;
942
+ }).catch(() => false);
943
+ return this.iterativeScanSupport;
944
+ }
945
+ };
946
+ var PgLexicalVectorStore = class extends PgVectorStore {
947
+ static {
948
+ __name(this, "PgLexicalVectorStore");
949
+ }
950
+ tsConfig;
951
+ tsColumn;
952
+ anyTermFallback;
953
+ stopWords;
954
+ constructor(client, options = {}) {
955
+ super(client, options);
956
+ const config = options.fullText?.config ?? "simple";
957
+ if (!SQL_IDENTIFIER.test(config)) {
958
+ throw new Error(`PgLexicalVectorStore: invalid text search configuration "${config}"`);
959
+ }
960
+ const column = options.fullText?.column;
961
+ if (column !== void 0 && !SQL_IDENTIFIER.test(column)) {
962
+ throw new Error(`PgLexicalVectorStore: invalid tsvector column "${column}"`);
963
+ }
964
+ this.tsConfig = config;
965
+ this.tsColumn = column;
966
+ this.anyTermFallback = options.fullText?.anyTermFallback ?? true;
967
+ const stopWords = options.fullText?.stopWords;
968
+ this.stopWords = stopWords === false ? {} : stopWords ?? DEFAULT_STOP_WORDS;
969
+ }
970
+ schemaStatements() {
971
+ const statements = super.schemaStatements();
972
+ if (this.tsColumn === void 0) {
973
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_text_tsv_idx
974
+ ON ${this.table} USING gin (${this.tsvector()})`);
975
+ }
976
+ return statements;
977
+ }
978
+ async searchText(query, options) {
979
+ if (filterMatchesNothing(options.filter) || query.trim() === "") {
980
+ return [];
981
+ }
982
+ if (!this.anyTermFallback || hasSearchSyntax(query)) {
983
+ const strict = await this.runTextSearch(query, options);
984
+ if (strict.length > 0 || !this.anyTermFallback) {
985
+ return strict;
986
+ }
987
+ }
988
+ const terms = keywordTerms(query, 24, this.stopWords);
989
+ return terms.length === 0 ? [] : this.runRankedTermSearch(terms, options);
990
+ }
991
+ /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */
992
+ async runTextSearch(query, options) {
993
+ const tsquery = `websearch_to_tsquery('${this.tsConfig}', $1)`;
994
+ const params = [
995
+ query,
996
+ options.topK
997
+ ];
998
+ const conditions = [
999
+ `${this.tsvector()} @@ ${tsquery}`,
1000
+ ...this.whereConditions(options.filter, params)
1001
+ ];
1002
+ const rows = await this.client.query(`SELECT id, text, source, metadata, ts_rank_cd(${this.tsvector()}, ${tsquery}) AS score
1003
+ FROM ${this.table}
1004
+ WHERE ${conditions.join(" AND ")}
1005
+ ORDER BY score DESC, id
1006
+ LIMIT $2`, params);
1007
+ return rows.map(toPassage);
1008
+ }
1009
+ /**
1010
+ * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,
1011
+ * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated
1012
+ * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full
1013
+ * of common words can match most of the table.
1014
+ */
1015
+ async runRankedTermSearch(terms, options) {
1016
+ const any = `to_tsquery('${this.tsConfig}', $1)`;
1017
+ const params = [
1018
+ anyTermTsquery(terms),
623
1019
  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
- }));
1020
+ terms
1021
+ ];
1022
+ const conditions = [
1023
+ `${this.tsvector()} @@ ${any}`,
1024
+ ...this.whereConditions(options.filter, params)
1025
+ ];
1026
+ const rows = await this.client.query(`WITH __terms AS (
1027
+ SELECT DISTINCT to_tsquery('${this.tsConfig}', quote_literal(t)) AS q
1028
+ FROM unnest($3::text[]) AS t
1029
+ ), __cand AS MATERIALIZED (
1030
+ SELECT id, text, source, metadata, ${this.tsvector()} AS __tsv
1031
+ FROM ${this.table}
1032
+ WHERE ${conditions.join(" AND ")}
1033
+ ), __hits AS MATERIALIZED (
1034
+ SELECT __cand.id AS __id, __terms.q FROM __cand JOIN __terms ON __cand.__tsv @@ __terms.q
1035
+ ), __idf AS (
1036
+ SELECT q, ln(1 + ((SELECT count(*) FROM __cand) - count(*) + 0.5) / (count(*) + 0.5)) AS w
1037
+ FROM __hits GROUP BY q
1038
+ ), __scored AS (
1039
+ SELECT __hits.__id, sum(__idf.w) AS s, rank() OVER (ORDER BY sum(__idf.w) DESC) AS r
1040
+ FROM __hits JOIN __idf USING (q) GROUP BY __hits.__id
1041
+ )
1042
+ SELECT __cand.id, __cand.text, __cand.source, __cand.metadata,
1043
+ __scored.s + ts_rank_cd(__cand.__tsv, ${any}, 32) AS score
1044
+ FROM __scored JOIN __cand ON __cand.id = __scored.__id
1045
+ WHERE __scored.r <= $2
1046
+ ORDER BY __scored.s DESC, ts_rank_cd(__cand.__tsv, ${any}, 32) DESC, __cand.id
1047
+ LIMIT $2`, params);
1048
+ return rows.map(toPassage);
1049
+ }
1050
+ /** Must be character-for-character the index expression, or the planner won't use the index. */
1051
+ tsvector() {
1052
+ return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;
637
1053
  }
638
1054
  };
639
- function buildWhere(filter, startIndex) {
1055
+ var SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
1056
+ function toPassage(row) {
1057
+ return {
1058
+ id: row.id,
1059
+ text: row.text,
1060
+ score: Number(row.score),
1061
+ ...row.source !== null ? {
1062
+ source: row.source
1063
+ } : {},
1064
+ ...row.metadata !== null ? {
1065
+ metadata: row.metadata
1066
+ } : {}
1067
+ };
1068
+ }
1069
+ __name(toPassage, "toPassage");
1070
+ function buildWhere(filter, params) {
640
1071
  if (filter === void 0 || Object.keys(filter).length === 0) {
641
- return {
642
- sql: "",
643
- params: []
644
- };
1072
+ return [];
645
1073
  }
646
1074
  const stripped = stripNulBytes(filter);
647
1075
  const clauses = [];
648
- const params = [];
649
1076
  const scalar = {};
650
- let index = startIndex;
651
1077
  for (const [key, value] of Object.entries(stripped)) {
652
1078
  if (Array.isArray(value)) {
653
1079
  if (value.length === 0) {
654
1080
  clauses.push("false");
655
1081
  continue;
656
1082
  }
657
- const keyParam = `$${index}`;
658
1083
  params.push(key);
659
- index += 1;
660
- const arrParam = `$${index}`;
1084
+ const keyParam = `$${params.length}`;
661
1085
  params.push(value.map(String));
662
- index += 1;
1086
+ const arrParam = `$${params.length}`;
663
1087
  clauses.push(`(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`);
664
1088
  } else {
665
1089
  scalar[key] = value;
666
1090
  }
667
1091
  }
668
1092
  if (Object.keys(scalar).length > 0) {
669
- clauses.push(`metadata @> $${index}::jsonb`);
670
1093
  params.push(JSON.stringify(scalar));
671
- index += 1;
1094
+ clauses.push(`metadata @> $${params.length}::jsonb`);
672
1095
  }
673
- return {
674
- sql: `WHERE ${clauses.join(" AND ")}`,
675
- params
676
- };
1096
+ return clauses;
677
1097
  }
678
1098
  __name(buildWhere, "buildWhere");
679
1099
  var DOCUMENT_ID_FROM_CHUNK = "regexp_replace(id, '#[0-9]+$', '')";
@@ -1483,7 +1903,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1483
1903
  attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);
1484
1904
  }
1485
1905
  const inlineScore = scoreSource === "lexical" ? toStr(reply[index + 1]) : void 0;
1486
- passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1906
+ passages.push(toPassage2(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1487
1907
  }
1488
1908
  }
1489
1909
  return passages;
@@ -1491,7 +1911,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1491
1911
  if (typeof reply === "object" && reply !== null && "results" in reply) {
1492
1912
  const results = reply.results;
1493
1913
  if (Array.isArray(results)) {
1494
- return results.map((result) => toPassage(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1914
+ return results.map((result) => toPassage2(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1495
1915
  }
1496
1916
  }
1497
1917
  return [];
@@ -1524,7 +1944,7 @@ function readAttributes(result) {
1524
1944
  return attrs;
1525
1945
  }
1526
1946
  __name(readAttributes, "readAttributes");
1527
- function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1947
+ function toPassage2(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1528
1948
  const score = scoreSource === "lexical" ? Number(inlineScore ?? "0") : 1 - Number(attrs.vector_score ?? "1");
1529
1949
  const source = attrs.source;
1530
1950
  return {
@@ -1537,7 +1957,7 @@ function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1537
1957
  ...parseMetadata(attrs.metadata_json)
1538
1958
  };
1539
1959
  }
1540
- __name(toPassage, "toPassage");
1960
+ __name(toPassage2, "toPassage");
1541
1961
  function parseMetadata(raw) {
1542
1962
  if (raw === void 0) {
1543
1963
  return {};
@@ -2082,14 +2502,182 @@ function createRetrievalTool(retriever, options = {}) {
2082
2502
  };
2083
2503
  }
2084
2504
  __name(createRetrievalTool, "createRetrievalTool");
2505
+
2506
+ // src/openai-embeddings.ts
2507
+ var HttpModelError = class extends Error {
2508
+ static {
2509
+ __name(this, "HttpModelError");
2510
+ }
2511
+ status;
2512
+ constructor(status, message) {
2513
+ super(message), this.status = status;
2514
+ this.name = "HttpModelError";
2515
+ }
2516
+ };
2517
+ function openAiEmbeddings(options) {
2518
+ const url = `${(options.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/embeddings`;
2519
+ const batchSize = Math.max(1, Math.floor(options.batchSize ?? 64));
2520
+ const maxInputChars = options.maxInputChars ?? 24e3;
2521
+ const timeoutMs = options.timeoutMs ?? 12e4;
2522
+ const doFetch = options.fetch ?? fetch;
2523
+ return {
2524
+ async embed(texts) {
2525
+ const vectors = [];
2526
+ for (let start = 0; start < texts.length; start += batchSize) {
2527
+ const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
2528
+ const response = await doFetch(url, {
2529
+ method: "POST",
2530
+ headers: {
2531
+ "content-type": "application/json",
2532
+ ...options.apiKey ? {
2533
+ authorization: `Bearer ${options.apiKey}`
2534
+ } : {},
2535
+ ...options.headers
2536
+ },
2537
+ body: JSON.stringify({
2538
+ model: options.model,
2539
+ input,
2540
+ ...options.dimensions !== void 0 ? {
2541
+ dimensions: options.dimensions
2542
+ } : {}
2543
+ }),
2544
+ signal: AbortSignal.timeout(timeoutMs)
2545
+ });
2546
+ const body = await response.text();
2547
+ if (!response.ok) {
2548
+ throw new HttpModelError(response.status, `Embeddings request failed (${response.status}): ${errorMessageOf(body)}`);
2549
+ }
2550
+ const json = parseJson(body);
2551
+ const items = [
2552
+ ...json?.data ?? []
2553
+ ].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
2554
+ if (items.length !== input.length || !items.every((item) => Array.isArray(item.embedding))) {
2555
+ throw new HttpModelError(502, `Embeddings returned ${items.length} vectors for ${input.length} inputs`);
2556
+ }
2557
+ for (const item of items) {
2558
+ vectors.push(item.embedding);
2559
+ }
2560
+ const tokens = json?.usage?.total_tokens ?? json?.usage?.prompt_tokens;
2561
+ if (typeof tokens === "number" && Number.isFinite(tokens)) {
2562
+ options.onUsage?.(tokens);
2563
+ }
2564
+ }
2565
+ return vectors;
2566
+ }
2567
+ };
2568
+ }
2569
+ __name(openAiEmbeddings, "openAiEmbeddings");
2570
+ function parseJson(body) {
2571
+ try {
2572
+ return JSON.parse(body);
2573
+ } catch {
2574
+ return null;
2575
+ }
2576
+ }
2577
+ __name(parseJson, "parseJson");
2578
+ function errorMessageOf(body) {
2579
+ const json = parseJson(body);
2580
+ const message = typeof json?.error === "string" ? json.error : json?.error?.message ?? json?.message ?? void 0;
2581
+ return typeof message === "string" ? message : body.slice(0, 300);
2582
+ }
2583
+ __name(errorMessageOf, "errorMessageOf");
2584
+
2585
+ // src/http-reranker.ts
2586
+ var HttpReranker = class {
2587
+ static {
2588
+ __name(this, "HttpReranker");
2589
+ }
2590
+ options;
2591
+ format;
2592
+ doFetch;
2593
+ constructor(options) {
2594
+ this.options = options;
2595
+ this.format = options.format ?? "auto";
2596
+ this.doFetch = options.fetch ?? fetch;
2597
+ }
2598
+ async rerank(query, passages, options = {}) {
2599
+ if (passages.length === 0) {
2600
+ return [];
2601
+ }
2602
+ const topK = options.topK ?? passages.length;
2603
+ const documents = passages.map((passage) => passage.text);
2604
+ const response = await this.doFetch(this.options.url, {
2605
+ method: "POST",
2606
+ headers: {
2607
+ "content-type": "application/json",
2608
+ ...this.options.apiKey ? {
2609
+ authorization: `Bearer ${this.options.apiKey}`
2610
+ } : {},
2611
+ ...this.options.headers
2612
+ },
2613
+ body: JSON.stringify({
2614
+ ...this.options.model !== void 0 ? {
2615
+ model: this.options.model
2616
+ } : {},
2617
+ query,
2618
+ ...this.format !== "tei" ? {
2619
+ documents,
2620
+ top_n: topK
2621
+ } : {},
2622
+ ...this.format !== "cohere" ? {
2623
+ texts: documents
2624
+ } : {}
2625
+ }),
2626
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
2627
+ });
2628
+ const body = await response.text();
2629
+ if (!response.ok) {
2630
+ throw new HttpModelError(response.status, `Rerank request failed (${response.status}): ${errorMessageOf(body)}`);
2631
+ }
2632
+ const items = itemsOf(body);
2633
+ if (items === void 0) {
2634
+ throw new HttpModelError(502, "Rerank response has no results");
2635
+ }
2636
+ const seen = /* @__PURE__ */ new Set();
2637
+ const reranked = [];
2638
+ for (const item of items) {
2639
+ const index = item.index;
2640
+ const passage = typeof index === "number" ? passages[index] : void 0;
2641
+ if (passage === void 0 || seen.has(index)) {
2642
+ continue;
2643
+ }
2644
+ seen.add(index);
2645
+ reranked.push({
2646
+ ...passage,
2647
+ score: Number(item.relevance_score ?? item.score ?? 0)
2648
+ });
2649
+ }
2650
+ reranked.sort((a, b) => b.score - a.score);
2651
+ return reranked.slice(0, topK);
2652
+ }
2653
+ };
2654
+ function itemsOf(body) {
2655
+ let json;
2656
+ try {
2657
+ json = JSON.parse(body);
2658
+ } catch {
2659
+ return void 0;
2660
+ }
2661
+ if (Array.isArray(json)) {
2662
+ return json;
2663
+ }
2664
+ const wrapped = json;
2665
+ const items = wrapped?.results ?? wrapped?.data;
2666
+ return Array.isArray(items) ? items : void 0;
2667
+ }
2668
+ __name(itemsOf, "itemsOf");
2085
2669
  // Annotate the CommonJS export names for ESM import in node:
2086
2670
  0 && (module.exports = {
2671
+ DEFAULT_STOP_WORDS,
2087
2672
  EmbeddingRetriever,
2088
2673
  FilteredRetriever,
2674
+ HttpModelError,
2675
+ HttpReranker,
2089
2676
  HybridRetriever,
2090
2677
  KeywordRetriever,
2091
2678
  LexicalRetriever,
2092
2679
  MemoryVectorStore,
2680
+ PgLexicalVectorStore,
2093
2681
  PgVectorStore,
2094
2682
  RAG_DIAGNOSTIC_LIB,
2095
2683
  RAG_RETRIEVAL_CHANNEL,
@@ -2098,6 +2686,7 @@ __name(createRetrievalTool, "createRetrievalTool");
2098
2686
  RedisVectorStore,
2099
2687
  RerankingRetriever,
2100
2688
  UnsafeRemovalError,
2689
+ anyTermTsquery,
2101
2690
  applyMetadataPatch,
2102
2691
  chunkDocuments,
2103
2692
  chunkIndexOf,
@@ -2109,9 +2698,13 @@ __name(createRetrievalTool, "createRetrievalTool");
2109
2698
  describesRetrieval,
2110
2699
  documentIdOf,
2111
2700
  emitRagRetrieval,
2701
+ hasSearchSyntax,
2112
2702
  ingestChunks,
2113
2703
  ingestDocuments,
2114
2704
  instrumentRetriever,
2115
- isLexicalVectorStore
2705
+ isLexicalVectorStore,
2706
+ keywordTerms,
2707
+ openAiEmbeddings,
2708
+ toPgClient
2116
2709
  });
2117
2710
  //# sourceMappingURL=index.cjs.map