@dudousxd/nestjs-agent-rag 0.6.0 → 0.7.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 +35 -1
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -2
- package/dist/index.d.ts +56 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.d.cts
CHANGED
|
@@ -595,6 +595,29 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
595
595
|
searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
interface EmbeddingRetrieverOptions {
|
|
599
|
+
/**
|
|
600
|
+
* Drop passages scoring below this, so a query the corpus cannot answer returns nothing.
|
|
601
|
+
*
|
|
602
|
+
* A vector search always returns its `topK` nearest neighbours, and "nearest" is not "relevant".
|
|
603
|
+
* Ask a corpus of maintenance policy about the dining hall menu and it hands back the five least
|
|
604
|
+
* unrelated paragraphs it has, each with a score, in confident rank order. Nothing in that result
|
|
605
|
+
* says the answer is absent, so without a floor the consumer either feeds a model five irrelevant
|
|
606
|
+
* passages or invents a threshold of its own.
|
|
607
|
+
*
|
|
608
|
+
* **This is the only place in the package where a threshold is meaningful, which is why it lives
|
|
609
|
+
* here and not on `HybridRetriever`.** The score here comes straight from the store's vector
|
|
610
|
+
* search: a calibrated similarity, comparable across queries and monotonic in how well the passage
|
|
611
|
+
* matches. A fused score is neither — see the score section on `HybridRetriever`. Threshold each
|
|
612
|
+
* leg, then fuse; never the other way round.
|
|
613
|
+
*
|
|
614
|
+
* Pick the number by measuring, not by taste: score questions you know the corpus answers against
|
|
615
|
+
* questions you know it does not, and put the floor in the gap between the two populations. It is
|
|
616
|
+
* specific to the embedding model and the distance metric, so it does not survive changing either.
|
|
617
|
+
* Left unset nothing is dropped, which is the previous behaviour exactly.
|
|
618
|
+
*/
|
|
619
|
+
minScore?: number;
|
|
620
|
+
}
|
|
598
621
|
/**
|
|
599
622
|
* Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:
|
|
600
623
|
* embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool
|
|
@@ -603,7 +626,8 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
603
626
|
declare class EmbeddingRetriever implements Retriever {
|
|
604
627
|
private readonly embedder;
|
|
605
628
|
private readonly store;
|
|
606
|
-
|
|
629
|
+
private readonly options;
|
|
630
|
+
constructor(embedder: EmbeddingProvider, store: VectorStore, options?: EmbeddingRetrieverOptions);
|
|
607
631
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
608
632
|
}
|
|
609
633
|
|
|
@@ -724,6 +748,36 @@ interface HybridRetrieverOptions {
|
|
|
724
748
|
* vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the
|
|
725
749
|
* lists it appears in, so it needs no score normalization between incompatible scales (cosine vs
|
|
726
750
|
* BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.
|
|
751
|
+
*
|
|
752
|
+
* ## The score on a fused passage is not a similarity. Do not threshold it.
|
|
753
|
+
*
|
|
754
|
+
* Read this before using `passage.score` for anything other than the order it already produced.
|
|
755
|
+
*
|
|
756
|
+
* Not normalizing between scales is what makes RRF robust, and the price is that the number it
|
|
757
|
+
* emits is a function of **rank alone** — it never sees a similarity at all. Two consequences that
|
|
758
|
+
* bite in practice:
|
|
759
|
+
*
|
|
760
|
+
* - **It is not comparable to the score on a passage from {@link EmbeddingRetriever}**, even though
|
|
761
|
+
* it arrives in the same field, of the same type, from the same `Retriever` interface. There is no
|
|
762
|
+
* discriminator; a consumer holding a `Passage[]` cannot tell which kind of number it has. If you
|
|
763
|
+
* surface scores to a user or a model, know which retriever produced them.
|
|
764
|
+
* - **Its range is narrow, fixed, and says nothing about quality.** Every possible score lies in
|
|
765
|
+
* `[minWeight / (k + fetchTopK), Σ weights / (k + 1)]` — with the defaults (`k = 60`,
|
|
766
|
+
* `fetchTopK = 20`, two unweighted legs) that is `[0.0125, 0.0328]`. A perfect match and the
|
|
767
|
+
* least-bad member of a set of terrible matches both come back around `0.016`.
|
|
768
|
+
*
|
|
769
|
+
* The second point is why a relevance floor cannot live here. What the fused score actually measures
|
|
770
|
+
* is how strongly the legs *agree* — and agreement tracks nothing about correctness. When both legs
|
|
771
|
+
* return the same nearest nothing for an unanswerable query, they reinforce it to the ceiling, the
|
|
772
|
+
* same ceiling a unanimous correct answer gets. Measured on one corpus, the dense leg alone
|
|
773
|
+
* separated cleanly (weakest real answer 0.244, loudest absent-topic answer 0.143) while those same
|
|
774
|
+
* queries after fusion scored 0.032 and 0.033 — inverted. On another corpus the inversion may not
|
|
775
|
+
* appear; what does not change is that both numbers live in a band roughly one part in fifty wide,
|
|
776
|
+
* with no room for a threshold to mean anything.
|
|
777
|
+
*
|
|
778
|
+
* So: put the floor on the legs, with {@link EmbeddingRetrieverOptions.minScore}, and fuse what
|
|
779
|
+
* survives. `HybridRetriever` deliberately has no `minScore` of its own; a threshold option here
|
|
780
|
+
* would be an invitation to apply it to a number that cannot support one.
|
|
727
781
|
*/
|
|
728
782
|
declare class HybridRetriever implements Retriever {
|
|
729
783
|
private readonly retrievers;
|
|
@@ -789,4 +843,4 @@ interface RetrievalTool {
|
|
|
789
843
|
*/
|
|
790
844
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
791
845
|
|
|
792
|
-
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
|
846
|
+
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -595,6 +595,29 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
595
595
|
searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
interface EmbeddingRetrieverOptions {
|
|
599
|
+
/**
|
|
600
|
+
* Drop passages scoring below this, so a query the corpus cannot answer returns nothing.
|
|
601
|
+
*
|
|
602
|
+
* A vector search always returns its `topK` nearest neighbours, and "nearest" is not "relevant".
|
|
603
|
+
* Ask a corpus of maintenance policy about the dining hall menu and it hands back the five least
|
|
604
|
+
* unrelated paragraphs it has, each with a score, in confident rank order. Nothing in that result
|
|
605
|
+
* says the answer is absent, so without a floor the consumer either feeds a model five irrelevant
|
|
606
|
+
* passages or invents a threshold of its own.
|
|
607
|
+
*
|
|
608
|
+
* **This is the only place in the package where a threshold is meaningful, which is why it lives
|
|
609
|
+
* here and not on `HybridRetriever`.** The score here comes straight from the store's vector
|
|
610
|
+
* search: a calibrated similarity, comparable across queries and monotonic in how well the passage
|
|
611
|
+
* matches. A fused score is neither — see the score section on `HybridRetriever`. Threshold each
|
|
612
|
+
* leg, then fuse; never the other way round.
|
|
613
|
+
*
|
|
614
|
+
* Pick the number by measuring, not by taste: score questions you know the corpus answers against
|
|
615
|
+
* questions you know it does not, and put the floor in the gap between the two populations. It is
|
|
616
|
+
* specific to the embedding model and the distance metric, so it does not survive changing either.
|
|
617
|
+
* Left unset nothing is dropped, which is the previous behaviour exactly.
|
|
618
|
+
*/
|
|
619
|
+
minScore?: number;
|
|
620
|
+
}
|
|
598
621
|
/**
|
|
599
622
|
* Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:
|
|
600
623
|
* embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool
|
|
@@ -603,7 +626,8 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
603
626
|
declare class EmbeddingRetriever implements Retriever {
|
|
604
627
|
private readonly embedder;
|
|
605
628
|
private readonly store;
|
|
606
|
-
|
|
629
|
+
private readonly options;
|
|
630
|
+
constructor(embedder: EmbeddingProvider, store: VectorStore, options?: EmbeddingRetrieverOptions);
|
|
607
631
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
608
632
|
}
|
|
609
633
|
|
|
@@ -724,6 +748,36 @@ interface HybridRetrieverOptions {
|
|
|
724
748
|
* vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the
|
|
725
749
|
* lists it appears in, so it needs no score normalization between incompatible scales (cosine vs
|
|
726
750
|
* BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.
|
|
751
|
+
*
|
|
752
|
+
* ## The score on a fused passage is not a similarity. Do not threshold it.
|
|
753
|
+
*
|
|
754
|
+
* Read this before using `passage.score` for anything other than the order it already produced.
|
|
755
|
+
*
|
|
756
|
+
* Not normalizing between scales is what makes RRF robust, and the price is that the number it
|
|
757
|
+
* emits is a function of **rank alone** — it never sees a similarity at all. Two consequences that
|
|
758
|
+
* bite in practice:
|
|
759
|
+
*
|
|
760
|
+
* - **It is not comparable to the score on a passage from {@link EmbeddingRetriever}**, even though
|
|
761
|
+
* it arrives in the same field, of the same type, from the same `Retriever` interface. There is no
|
|
762
|
+
* discriminator; a consumer holding a `Passage[]` cannot tell which kind of number it has. If you
|
|
763
|
+
* surface scores to a user or a model, know which retriever produced them.
|
|
764
|
+
* - **Its range is narrow, fixed, and says nothing about quality.** Every possible score lies in
|
|
765
|
+
* `[minWeight / (k + fetchTopK), Σ weights / (k + 1)]` — with the defaults (`k = 60`,
|
|
766
|
+
* `fetchTopK = 20`, two unweighted legs) that is `[0.0125, 0.0328]`. A perfect match and the
|
|
767
|
+
* least-bad member of a set of terrible matches both come back around `0.016`.
|
|
768
|
+
*
|
|
769
|
+
* The second point is why a relevance floor cannot live here. What the fused score actually measures
|
|
770
|
+
* is how strongly the legs *agree* — and agreement tracks nothing about correctness. When both legs
|
|
771
|
+
* return the same nearest nothing for an unanswerable query, they reinforce it to the ceiling, the
|
|
772
|
+
* same ceiling a unanimous correct answer gets. Measured on one corpus, the dense leg alone
|
|
773
|
+
* separated cleanly (weakest real answer 0.244, loudest absent-topic answer 0.143) while those same
|
|
774
|
+
* queries after fusion scored 0.032 and 0.033 — inverted. On another corpus the inversion may not
|
|
775
|
+
* appear; what does not change is that both numbers live in a band roughly one part in fifty wide,
|
|
776
|
+
* with no room for a threshold to mean anything.
|
|
777
|
+
*
|
|
778
|
+
* So: put the floor on the legs, with {@link EmbeddingRetrieverOptions.minScore}, and fuse what
|
|
779
|
+
* survives. `HybridRetriever` deliberately has no `minScore` of its own; a threshold option here
|
|
780
|
+
* would be an invitation to apply it to a number that cannot support one.
|
|
727
781
|
*/
|
|
728
782
|
declare class HybridRetriever implements Retriever {
|
|
729
783
|
private readonly retrievers;
|
|
@@ -789,4 +843,4 @@ interface RetrievalTool {
|
|
|
789
843
|
*/
|
|
790
844
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
791
845
|
|
|
792
|
-
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
|
846
|
+
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
package/dist/index.js
CHANGED
|
@@ -1295,9 +1295,11 @@ var EmbeddingRetriever = class {
|
|
|
1295
1295
|
}
|
|
1296
1296
|
embedder;
|
|
1297
1297
|
store;
|
|
1298
|
-
|
|
1298
|
+
options;
|
|
1299
|
+
constructor(embedder, store, options = {}) {
|
|
1299
1300
|
this.embedder = embedder;
|
|
1300
1301
|
this.store = store;
|
|
1302
|
+
this.options = options;
|
|
1301
1303
|
}
|
|
1302
1304
|
async retrieve(query, options = {}) {
|
|
1303
1305
|
const [embedding] = await this.embedder.embed([
|
|
@@ -1306,12 +1308,14 @@ var EmbeddingRetriever = class {
|
|
|
1306
1308
|
if (embedding === void 0) {
|
|
1307
1309
|
return [];
|
|
1308
1310
|
}
|
|
1309
|
-
|
|
1311
|
+
const passages = await this.store.search(embedding, {
|
|
1310
1312
|
topK: options.topK ?? 5,
|
|
1311
1313
|
...options.filter !== void 0 ? {
|
|
1312
1314
|
filter: options.filter
|
|
1313
1315
|
} : {}
|
|
1314
1316
|
});
|
|
1317
|
+
const { minScore } = this.options;
|
|
1318
|
+
return minScore === void 0 ? passages : passages.filter((passage) => passage.score >= minScore);
|
|
1315
1319
|
}
|
|
1316
1320
|
};
|
|
1317
1321
|
|