@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.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
|
-
|
|
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
|
-
/**
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
409
|
-
)`
|
|
410
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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 (
|
|
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
|
|
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
|
|
471
|
-
ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`,
|
|
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
|
|
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
|
|
526
|
-
ORDER BY doc_id`,
|
|
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
|
|
551
|
-
const
|
|
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
|
|
559
|
-
const
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
${
|
|
568
|
-
ORDER BY
|
|
569
|
-
LIMIT $2
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
const arrParam = `$${index}`;
|
|
905
|
+
const keyParam = `$${params.length}`;
|
|
609
906
|
params.push(value.map(String));
|
|
610
|
-
|
|
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
|
-
|
|
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(
|
|
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) =>
|
|
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
|
|
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(
|
|
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 "
|
|
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
|