@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/README.md +90 -14
- package/dist/index.cjs +541 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +287 -23
- package/dist/index.d.ts +287 -23
- package/dist/index.js +536 -74
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
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
|
-
|
|
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
|
-
/**
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
461
|
-
)`
|
|
462
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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 (
|
|
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
|
|
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
|
|
523
|
-
ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`,
|
|
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
|
|
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
|
|
578
|
-
ORDER BY doc_id`,
|
|
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
|
|
603
|
-
const
|
|
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
|
|
611
|
-
const
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
${
|
|
620
|
-
ORDER BY
|
|
621
|
-
LIMIT $2
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
|
|
660
|
-
const arrParam = `$${index}`;
|
|
962
|
+
const keyParam = `$${params.length}`;
|
|
661
963
|
params.push(value.map(String));
|
|
662
|
-
|
|
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
|
-
|
|
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(
|
|
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) =>
|
|
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
|
|
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(
|
|
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
|