@dudousxd/nestjs-agent-rag 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.cjs +883 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +845 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { Passage, Retriever, EmbeddingProvider, RetrieveOptions, Reranker, ToolSpec, ToolHandler } from '@dudousxd/nestjs-agent-core';
|
|
2
|
+
|
|
3
|
+
interface ChunkOptions {
|
|
4
|
+
/** Target max characters per chunk. Default 800. */
|
|
5
|
+
chunkSize?: number;
|
|
6
|
+
/** Characters of overlap carried from the end of one chunk into the next. Default 100. */
|
|
7
|
+
overlap?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Splits text into overlapping chunks for embedding. Greedy up to `chunkSize`, but prefers to break
|
|
11
|
+
* on a paragraph → sentence → word boundary in the back half of the window (so a chunk rarely cuts
|
|
12
|
+
* mid-sentence), then carries `overlap` characters into the next chunk to preserve context across
|
|
13
|
+
* the seam. Pure and deterministic.
|
|
14
|
+
*/
|
|
15
|
+
declare function chunkText(text: string, options?: ChunkOptions): string[];
|
|
16
|
+
|
|
17
|
+
/** A stored, embedded chunk. `embedding` length must match the store's configured dimensions. */
|
|
18
|
+
interface VectorRecord {
|
|
19
|
+
id: string;
|
|
20
|
+
text: string;
|
|
21
|
+
embedding: number[];
|
|
22
|
+
/** Citation-facing origin (document title, URL, row id). */
|
|
23
|
+
source?: string;
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
interface VectorSearchOptions {
|
|
27
|
+
topK: number;
|
|
28
|
+
/** Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`. */
|
|
29
|
+
filter?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The write + search side of RAG storage. `@dudousxd/nestjs-agent-rag` ships `MemoryVectorStore`
|
|
33
|
+
* (in-JS cosine, tests + small scale) and `PgVectorStore` (pgvector); any impl works. Pair one with
|
|
34
|
+
* an `EmbeddingProvider` via {@link import('./embedding-retriever.js').EmbeddingRetriever} to get a
|
|
35
|
+
* core `Retriever`.
|
|
36
|
+
*/
|
|
37
|
+
interface VectorStore {
|
|
38
|
+
upsert(records: VectorRecord[]): Promise<void>;
|
|
39
|
+
search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Delete every chunk belonging to a source document — all records whose id is `${documentId}`
|
|
42
|
+
* or `${documentId}#<n>` (the id scheme {@link import('./ingest.js').chunkDocuments} produces).
|
|
43
|
+
* Use it to drop a document from the index, and before re-ingesting one: `upsert` overwrites
|
|
44
|
+
* matching ids but can't remove chunks a shorter new version no longer produces, so a re-ingest
|
|
45
|
+
* without a preceding `remove` leaves the old tail orphaned.
|
|
46
|
+
*/
|
|
47
|
+
remove(documentId: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* List the distinct source documents currently indexed (chunk ids collapsed back to their document
|
|
50
|
+
* by stripping the trailing `#<n>`), each with a representative chunk's `metadata`, optionally
|
|
51
|
+
* narrowed by a metadata `filter`. The enumeration seam for reconciliation: diff this against your
|
|
52
|
+
* source of truth to find documents to ingest (missing), re-ingest (a stored fingerprint like
|
|
53
|
+
* `size` changed), or {@link VectorStore.remove remove} (orphaned).
|
|
54
|
+
*/
|
|
55
|
+
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
56
|
+
}
|
|
57
|
+
/** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */
|
|
58
|
+
interface IndexedDocument {
|
|
59
|
+
id: string;
|
|
60
|
+
metadata?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */
|
|
63
|
+
declare function documentIdOf(chunkId: string): string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter
|
|
67
|
+
* for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).
|
|
68
|
+
*/
|
|
69
|
+
declare class MemoryVectorStore implements VectorStore {
|
|
70
|
+
private readonly records;
|
|
71
|
+
upsert(records: VectorRecord[]): Promise<void>;
|
|
72
|
+
remove(documentId: string): Promise<void>;
|
|
73
|
+
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
74
|
+
search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The minimal Postgres surface {@link PgVectorStore} needs — adapt your own `pg` / `postgres.js`
|
|
79
|
+
* client to it, so this package pulls in NO driver (bring your own, like the store adapters take an
|
|
80
|
+
* ORM handle and the Redis transport takes a client). `query` runs parameterized SQL (`$1,$2,…`) and
|
|
81
|
+
* resolves the result rows.
|
|
82
|
+
*/
|
|
83
|
+
interface PgClient {
|
|
84
|
+
query<Row = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<Row[]>;
|
|
85
|
+
}
|
|
86
|
+
interface PgVectorStoreOptions {
|
|
87
|
+
/** Table name. Default `agent_rag_chunks`. */
|
|
88
|
+
table?: string;
|
|
89
|
+
/** Embedding width — must match your model (e.g. 1536 for text-embedding-3-small). Default 1536. */
|
|
90
|
+
dimensions?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A pgvector-backed {@link VectorStore} — the production reference adapter. Cosine distance via the
|
|
94
|
+
* `<=>` operator over an HNSW index; metadata in a `jsonb` column filtered with `@>`. Call
|
|
95
|
+
* {@link PgVectorStore.ensureSchema} once at boot to create the extension, table, and index.
|
|
96
|
+
*/
|
|
97
|
+
declare class PgVectorStore implements VectorStore {
|
|
98
|
+
private readonly client;
|
|
99
|
+
private readonly table;
|
|
100
|
+
private readonly dimensions;
|
|
101
|
+
constructor(client: PgClient, options?: PgVectorStoreOptions);
|
|
102
|
+
/** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
|
|
103
|
+
ensureSchema(): Promise<void>;
|
|
104
|
+
upsert(records: VectorRecord[]): Promise<void>;
|
|
105
|
+
remove(documentId: string): Promise<void>;
|
|
106
|
+
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
107
|
+
search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The minimal Redis surface {@link RedisVectorStore} needs: send a raw command and get the reply.
|
|
112
|
+
* Both `node-redis` (`client.sendCommand(args)`) and `ioredis` (wrap `redis.call`) satisfy it — so
|
|
113
|
+
* this package pulls in NO Redis driver (bring your own, like the other adapters). Requires the
|
|
114
|
+
* RediSearch module (Redis Stack / Redis 8+).
|
|
115
|
+
*/
|
|
116
|
+
interface RedisSearchClient {
|
|
117
|
+
sendCommand(args: (string | Buffer)[]): Promise<unknown>;
|
|
118
|
+
}
|
|
119
|
+
interface RedisVectorStoreOptions {
|
|
120
|
+
/** RediSearch index name. Default `agent_rag_idx`. */
|
|
121
|
+
index?: string;
|
|
122
|
+
/** Key prefix for the per-chunk hashes. Default `agent_rag:`. */
|
|
123
|
+
prefix?: string;
|
|
124
|
+
/** Embedding width — must match your model (e.g. 1536). Default 1536. */
|
|
125
|
+
dimensions?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Metadata keys to index as filterable TAG fields. RediSearch needs filter fields declared up
|
|
128
|
+
* front, so only keys listed here can be passed in `search`'s `filter`. Default none.
|
|
129
|
+
*/
|
|
130
|
+
filterableFields?: string[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A RediSearch-backed {@link VectorStore} — HNSW + cosine over Redis, the ecosystem-native option
|
|
134
|
+
* for anyone already running Redis (see `-transport-redis`). Chunks are hashes under `prefix`, the
|
|
135
|
+
* embedding a FLOAT32 vector field; `search` is a KNN query. Call {@link RedisVectorStore.ensureSchema}
|
|
136
|
+
* once at boot to create the index.
|
|
137
|
+
*/
|
|
138
|
+
declare class RedisVectorStore implements VectorStore {
|
|
139
|
+
private readonly client;
|
|
140
|
+
private readonly index;
|
|
141
|
+
private readonly prefix;
|
|
142
|
+
private readonly dimensions;
|
|
143
|
+
private readonly filterableFields;
|
|
144
|
+
constructor(client: RedisSearchClient, options?: RedisVectorStoreOptions);
|
|
145
|
+
/** Idempotent: create the HNSW/cosine index if `FT.INFO` says it doesn't exist yet. */
|
|
146
|
+
ensureSchema(): Promise<void>;
|
|
147
|
+
upsert(records: VectorRecord[]): Promise<void>;
|
|
148
|
+
remove(documentId: string): Promise<void>;
|
|
149
|
+
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
150
|
+
search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:
|
|
155
|
+
* embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool
|
|
156
|
+
* via `createRetrievalTool`, or into `AgentModule.forRoot({ retrieval })` for inject mode).
|
|
157
|
+
*/
|
|
158
|
+
declare class EmbeddingRetriever implements Retriever {
|
|
159
|
+
private readonly embedder;
|
|
160
|
+
private readonly store;
|
|
161
|
+
constructor(embedder: EmbeddingProvider, store: VectorStore);
|
|
162
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A source document to ingest. `id` scopes the chunk ids (`${id}#${n}`); `source` is the citation. */
|
|
166
|
+
interface IngestDocument {
|
|
167
|
+
id: string;
|
|
168
|
+
text: string;
|
|
169
|
+
source?: string;
|
|
170
|
+
metadata?: Record<string, unknown>;
|
|
171
|
+
}
|
|
172
|
+
/** A chunked, not-yet-embedded record. Feed the same array to a vector store AND a keyword index so
|
|
173
|
+
* both retrievers key on identical chunk ids (which is what lets hybrid fusion line them up). */
|
|
174
|
+
interface ChunkRecord {
|
|
175
|
+
id: string;
|
|
176
|
+
text: string;
|
|
177
|
+
source?: string;
|
|
178
|
+
metadata?: Record<string, unknown>;
|
|
179
|
+
}
|
|
180
|
+
interface IngestChunksOptions {
|
|
181
|
+
embedder: EmbeddingProvider;
|
|
182
|
+
store: VectorStore;
|
|
183
|
+
}
|
|
184
|
+
interface IngestOptions extends ChunkOptions, IngestChunksOptions {
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Split documents into chunk records. Chunk ids are `${doc.id}#${index}`, so re-chunking the same
|
|
188
|
+
* document produces the same ids (upsert overwrites in place rather than duplicating).
|
|
189
|
+
*/
|
|
190
|
+
declare function chunkDocuments(documents: IngestDocument[], options?: ChunkOptions): ChunkRecord[];
|
|
191
|
+
/** Embed pre-chunked records (one batched `embed`) and upsert them. Returns the record count. */
|
|
192
|
+
declare function ingestChunks(chunks: ChunkRecord[], options: IngestChunksOptions): Promise<number>;
|
|
193
|
+
/** Chunk → embed → upsert in one call. For hybrid search, use {@link chunkDocuments} +
|
|
194
|
+
* {@link ingestChunks} and feed the same chunks to a {@link import('./keyword-retriever.js').KeywordRetriever}. */
|
|
195
|
+
declare function ingestDocuments(documents: IngestDocument[], options: IngestOptions): Promise<number>;
|
|
196
|
+
|
|
197
|
+
interface KeywordRetrieverOptions {
|
|
198
|
+
/** BM25 term-frequency saturation. Default 1.5. */
|
|
199
|
+
k1?: number;
|
|
200
|
+
/** BM25 length normalization, 0 (off) … 1 (full). Default 0.75. */
|
|
201
|
+
b?: number;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* An in-memory BM25 lexical retriever — the keyword half of hybrid search. Feed it the SAME
|
|
205
|
+
* {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids
|
|
206
|
+
* line up and `HybridRetriever` can fuse the two rankings. Full BM25: idf, term-frequency saturation
|
|
207
|
+
* (`k1`), and document-length normalization (`b`).
|
|
208
|
+
*/
|
|
209
|
+
declare class KeywordRetriever implements Retriever {
|
|
210
|
+
private readonly docs;
|
|
211
|
+
private readonly documentFrequency;
|
|
212
|
+
private totalLength;
|
|
213
|
+
private readonly k1;
|
|
214
|
+
private readonly b;
|
|
215
|
+
constructor(options?: KeywordRetrieverOptions);
|
|
216
|
+
/** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */
|
|
217
|
+
add(records: ChunkRecord[]): void;
|
|
218
|
+
private remove;
|
|
219
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface HybridRetrieverOptions {
|
|
223
|
+
/** RRF constant — larger flattens the rank weighting. Default 60 (the value from the original paper). */
|
|
224
|
+
k?: number;
|
|
225
|
+
/** Candidates pulled from each retriever before fusion. Default 20. */
|
|
226
|
+
fetchTopK?: number;
|
|
227
|
+
/** Per-retriever multipliers on each RRF contribution (same order as `retrievers`). Default all 1. */
|
|
228
|
+
weights?: number[];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Fuses several retrievers with Reciprocal Rank Fusion — the robust way to combine, say, dense
|
|
232
|
+
* vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the
|
|
233
|
+
* lists it appears in, so it needs no score normalization between incompatible scales (cosine vs
|
|
234
|
+
* BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.
|
|
235
|
+
*/
|
|
236
|
+
declare class HybridRetriever implements Retriever {
|
|
237
|
+
private readonly retrievers;
|
|
238
|
+
private readonly options;
|
|
239
|
+
constructor(retrievers: Retriever[], options?: HybridRetrieverOptions);
|
|
240
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface RerankingRetrieverOptions {
|
|
244
|
+
/** How many candidates to pull from the base retriever before reranking. Default 20. */
|
|
245
|
+
fetchTopK?: number;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Two-stage retrieval: over-fetch cheap candidates from a base `Retriever`, then reorder them with a
|
|
249
|
+
* stronger {@link Reranker} and keep the top few. The standard precision boost — a fast first stage
|
|
250
|
+
* casts a wide net, a slow accurate second stage sharpens it. Composes over ANY retriever (vector,
|
|
251
|
+
* hybrid, keyword), and is itself a `Retriever`.
|
|
252
|
+
*/
|
|
253
|
+
declare class RerankingRetriever implements Retriever {
|
|
254
|
+
private readonly base;
|
|
255
|
+
private readonly reranker;
|
|
256
|
+
private readonly options;
|
|
257
|
+
constructor(base: Retriever, reranker: Reranker, options?: RerankingRetrieverOptions);
|
|
258
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Wraps a `Retriever` so every `retrieve` call ANDs a fixed metadata `filter` on top of whatever the
|
|
263
|
+
* caller passes — the owner/tenant-scoping primitive. `new FilteredRetriever(base, { ownerId })` only
|
|
264
|
+
* ever returns that owner's passages, so a shared vector store serves per-user / per-tenant RAG
|
|
265
|
+
* without the agent (or a crafted query) reaching across the boundary.
|
|
266
|
+
*
|
|
267
|
+
* The fixed filter takes precedence on key conflicts: a caller can narrow further but cannot widen
|
|
268
|
+
* past the scope. Composes over ANY retriever (vector, hybrid, reranked) and is itself a `Retriever`.
|
|
269
|
+
*/
|
|
270
|
+
declare class FilteredRetriever implements Retriever {
|
|
271
|
+
private readonly base;
|
|
272
|
+
private readonly filter;
|
|
273
|
+
constructor(base: Retriever, filter: Record<string, unknown>);
|
|
274
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
interface RetrievalToolOptions {
|
|
278
|
+
/** Tool name the model sees. Default `search_knowledge`. */
|
|
279
|
+
name?: string;
|
|
280
|
+
description?: string;
|
|
281
|
+
/** Passages per search. Default lets the retriever decide (typically 5). */
|
|
282
|
+
topK?: number;
|
|
283
|
+
}
|
|
284
|
+
/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */
|
|
285
|
+
interface RetrievalTool {
|
|
286
|
+
spec: ToolSpec;
|
|
287
|
+
handler: ToolHandler;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Builds the agentic-retrieval tool: a `read`-kind `search_knowledge(query)` the model calls when it
|
|
291
|
+
* decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources
|
|
292
|
+
* flow to the frontend and telescope through the normal tool-call machinery (the citation surface).
|
|
293
|
+
*
|
|
294
|
+
* ```ts
|
|
295
|
+
* providers: [provideAgentTool(createRetrievalTool(retriever))]
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
299
|
+
|
|
300
|
+
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, MemoryVectorStore, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type VectorRecord, type VectorSearchOptions, type VectorStore, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments };
|