@dudousxd/nestjs-agent-rag 0.8.0 → 0.10.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 +88 -2
- package/dist/index.cjs +347 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +345 -1
- package/dist/index.d.ts +345 -1
- package/dist/index.js +337 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.d.cts
CHANGED
|
@@ -174,6 +174,32 @@ interface VectorStore {
|
|
|
174
174
|
* `size` changed), or {@link VectorStore.remove remove} (orphaned).
|
|
175
175
|
*/
|
|
176
176
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Read one document's chunks back — id, text, metadata — ordered by their position in the
|
|
179
|
+
* document, optionally paged with `limit`/`offset`.
|
|
180
|
+
*
|
|
181
|
+
* The gap this closes is that **the text a store holds is otherwise write-only**. Every other way
|
|
182
|
+
* back to it is a ranked query: {@link VectorStore.search} needs an embedding,
|
|
183
|
+
* {@link LexicalVectorStore.searchText} needs a query string, and both return whatever *matched*,
|
|
184
|
+
* in relevance order, capped at `topK`. Neither can answer "show me what was indexed for this
|
|
185
|
+
* document, in order" — the question you ask when retrieval returns the wrong passage and you need
|
|
186
|
+
* to know whether the chunker cut badly or the retriever chose badly. Without this, the only way to
|
|
187
|
+
* see stored text is to reach around the adapter into the backing engine, which is exactly the
|
|
188
|
+
* abstraction break a consumer adopts a store to avoid.
|
|
189
|
+
*
|
|
190
|
+
* Document-scoped rather than filter-scoped, matching {@link VectorStore.remove} and
|
|
191
|
+
* {@link VectorStore.updateMetadata}: ordering is the whole point, and "position within the
|
|
192
|
+
* document" is only meaningful once a document has been picked. Enumerate documents with
|
|
193
|
+
* {@link VectorStore.listDocuments} first when you need to sweep.
|
|
194
|
+
*
|
|
195
|
+
* An unknown `documentId` yields `[]`; it does not throw, matching `remove` and `updateMetadata`.
|
|
196
|
+
*
|
|
197
|
+
* NOT an access-control seam. It takes no metadata `filter` and applies none, so it cannot enforce
|
|
198
|
+
* one — a caller that gates retrieval on a metadata field (tenant, audience, collection) MUST
|
|
199
|
+
* resolve that gate itself before calling, exactly as it would before `remove`. It is deliberately
|
|
200
|
+
* shaped for an operator/debug surface, not for serving end users.
|
|
201
|
+
*/
|
|
202
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
177
203
|
/**
|
|
178
204
|
* The distinct source document ids currently indexed, optionally narrowed by a metadata `filter` —
|
|
179
205
|
* {@link VectorStore.listDocuments} without the metadata. Use it when you only need the id set (a
|
|
@@ -271,9 +297,90 @@ interface IndexedDocument {
|
|
|
271
297
|
id: string;
|
|
272
298
|
metadata?: Record<string, unknown>;
|
|
273
299
|
}
|
|
300
|
+
/** One stored chunk, read back verbatim — what {@link VectorStore.listChunks} yields. */
|
|
301
|
+
interface StoredChunk {
|
|
302
|
+
/** The chunk id: `${documentId}#<n>`, or the bare document id for a single-chunk document. */
|
|
303
|
+
id: string;
|
|
304
|
+
/** Zero-based position within the document, parsed from the id. A bare id is chunk `0`. */
|
|
305
|
+
index: number;
|
|
306
|
+
/** The chunk text as it was indexed — the exact string the embedding was computed from. */
|
|
307
|
+
text: string;
|
|
308
|
+
metadata?: Record<string, unknown>;
|
|
309
|
+
}
|
|
310
|
+
/** Paging for {@link VectorStore.listChunks}. Omit both to read the whole document. */
|
|
311
|
+
interface ListChunksOptions {
|
|
312
|
+
/** Max chunks to return, applied after ordering by {@link StoredChunk.index}. */
|
|
313
|
+
limit?: number;
|
|
314
|
+
/** Chunks to skip, applied after ordering. */
|
|
315
|
+
offset?: number;
|
|
316
|
+
}
|
|
317
|
+
/** Chunk `n` of `${documentId}#<n>`; a bare document id (single-chunk document) is `0`. */
|
|
318
|
+
declare function chunkIndexOf(chunkId: string): number;
|
|
274
319
|
/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */
|
|
275
320
|
declare function documentIdOf(chunkId: string): string;
|
|
276
321
|
|
|
322
|
+
/**
|
|
323
|
+
* What a retrieval was served BY — the static half of retrieval telemetry, separated from the
|
|
324
|
+
* emitting half (`retrieval-telemetry.ts`) so a store or a retriever can describe itself without
|
|
325
|
+
* importing anything that emits. That separation is not tidiness: `retrieval-telemetry.ts` imports
|
|
326
|
+
* the retrievers' shapes to detect them, and the retrievers import their descriptor kind, so putting
|
|
327
|
+
* both halves in one module would make every retriever ↔ telemetry edge a cycle.
|
|
328
|
+
*/
|
|
329
|
+
/** Which retrieval strategy answered. `unknown` is a retriever this package did not ship. */
|
|
330
|
+
type RetrieverKind = 'embedding' | 'keyword' | 'lexical' | 'hybrid' | 'filtered' | 'reranking' | 'unknown';
|
|
331
|
+
/** Which backend held the chunks. Omitted entirely for a retriever that holds its own (keyword). */
|
|
332
|
+
type VectorStoreKind = 'memory' | 'pg' | 'redis';
|
|
333
|
+
/**
|
|
334
|
+
* A retriever's (or a store's) self-description, as it appears on a retrieval event. Every field is
|
|
335
|
+
* optional because the describer is answering only for the layer it *is*: a store knows its backend
|
|
336
|
+
* and namespace but not which strategy queried it; `KeywordRetriever` knows its strategy but has no
|
|
337
|
+
* store at all.
|
|
338
|
+
*/
|
|
339
|
+
interface RetrievalDescriptor {
|
|
340
|
+
retriever?: RetrieverKind;
|
|
341
|
+
store?: VectorStoreKind;
|
|
342
|
+
/**
|
|
343
|
+
* The namespace the chunks live in — `PgVectorStore`'s table, `RedisVectorStore`'s RediSearch
|
|
344
|
+
* index. This is the STORE-level namespace, not a per-query metadata scope: a host that partitions
|
|
345
|
+
* one index into logical collections with a metadata filter (the usual multi-tenant shape) reports
|
|
346
|
+
* that through `InstrumentRetrieverOptions.collection`, which overrides this.
|
|
347
|
+
*/
|
|
348
|
+
collection?: string;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The optional capability a store or retriever implements to appear on a retrieval event as
|
|
352
|
+
* something other than `unknown`. Deliberately NOT a member of `VectorStore` or of core's
|
|
353
|
+
* `Retriever`: both are implemented outside this package, and adding a required member to either
|
|
354
|
+
* would break every existing implementation on upgrade — for a field that only observability reads.
|
|
355
|
+
*/
|
|
356
|
+
interface DescribesRetrieval {
|
|
357
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
358
|
+
}
|
|
359
|
+
/** Structural guard, so a `VectorStore`/`Retriever` is narrowed to the capability without a cast. */
|
|
360
|
+
declare function describesRetrieval(value: unknown): value is DescribesRetrieval;
|
|
361
|
+
/** A candidate's descriptor, or an empty one when it does not carry the capability. */
|
|
362
|
+
declare function describeRetrieval(candidate: unknown): RetrievalDescriptor;
|
|
363
|
+
/**
|
|
364
|
+
* The store half of a wrapped retriever's descriptor: the base's `store`/`collection` with its
|
|
365
|
+
* `retriever` dropped, so the wrapper can stamp its own strategy on top.
|
|
366
|
+
*
|
|
367
|
+
* `new FilteredRetriever(new EmbeddingRetriever(embedder, pg), scope)` therefore reports
|
|
368
|
+
* `{ retriever: 'filtered', store: 'pg', collection: 'agent_rag_chunks' }` rather than losing the
|
|
369
|
+
* store: which backend answered is the thing a "retrievals by store" panel exists to show, and a
|
|
370
|
+
* production wiring is almost always two or three wrappers deep.
|
|
371
|
+
*/
|
|
372
|
+
declare function describeSource(base: unknown): Omit<RetrievalDescriptor, 'retriever'>;
|
|
373
|
+
/**
|
|
374
|
+
* The store half shared by EVERY leg of a composed retriever, or nothing when they disagree.
|
|
375
|
+
*
|
|
376
|
+
* A hybrid whose legs are the dense and lexical halves of the same RediSearch index (the shape
|
|
377
|
+
* `LexicalRetriever` exists for) genuinely was served by that one store, and reporting it is what
|
|
378
|
+
* makes the store breakdown add up. A hybrid fusing pgvector with an in-process BM25 index was not
|
|
379
|
+
* served by either alone, and picking the first leg would silently attribute every such retrieval to
|
|
380
|
+
* a store that answered half of it.
|
|
381
|
+
*/
|
|
382
|
+
declare function describeSharedSource(legs: readonly unknown[]): Omit<RetrievalDescriptor, 'retriever'>;
|
|
383
|
+
|
|
277
384
|
/**
|
|
278
385
|
* An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter
|
|
279
386
|
* for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).
|
|
@@ -286,6 +393,12 @@ declare function documentIdOf(chunkId: string): string;
|
|
|
286
393
|
*/
|
|
287
394
|
declare class MemoryVectorStore implements VectorStore {
|
|
288
395
|
private readonly records;
|
|
396
|
+
/**
|
|
397
|
+
* Telemetry self-description. No `collection`: there is no namespace to name — the corpus IS this
|
|
398
|
+
* instance, and two `MemoryVectorStore`s in one process are two corpora with nothing to tell them
|
|
399
|
+
* apart on a dashboard.
|
|
400
|
+
*/
|
|
401
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
289
402
|
upsert(records: VectorRecord[]): Promise<void>;
|
|
290
403
|
remove(documentId: string): Promise<void>;
|
|
291
404
|
/**
|
|
@@ -296,6 +409,16 @@ declare class MemoryVectorStore implements VectorStore {
|
|
|
296
409
|
*/
|
|
297
410
|
updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
|
|
298
411
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
412
|
+
/**
|
|
413
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
414
|
+
*
|
|
415
|
+
* Insertion order is not document order — `upsert` stores whatever the caller passed, and a
|
|
416
|
+
* re-ingest that rewrites chunk 3 alone moves it to the end of the Map. So this sorts by the index
|
|
417
|
+
* parsed from the id, the same thing the SQL and Redis adapters order by, rather than trusting the
|
|
418
|
+
* Map. A reference adapter that returned "usually ordered" would let a consumer ship a bug that
|
|
419
|
+
* only shows up on the store tests are NOT written against.
|
|
420
|
+
*/
|
|
421
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
299
422
|
listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
|
|
300
423
|
removeMany(documentIds: string[]): Promise<void>;
|
|
301
424
|
/**
|
|
@@ -356,6 +479,8 @@ declare class PgVectorStore implements VectorStore {
|
|
|
356
479
|
private readonly table;
|
|
357
480
|
private readonly dimensions;
|
|
358
481
|
constructor(client: PgClient, options?: PgVectorStoreOptions);
|
|
482
|
+
/** Telemetry self-description — the table is the namespace a retrieval was served from. */
|
|
483
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
359
484
|
/** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
|
|
360
485
|
ensureSchema(): Promise<void>;
|
|
361
486
|
upsert(records: VectorRecord[]): Promise<void>;
|
|
@@ -374,6 +499,19 @@ declare class PgVectorStore implements VectorStore {
|
|
|
374
499
|
*/
|
|
375
500
|
updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
|
|
376
501
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
502
|
+
/**
|
|
503
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
504
|
+
*
|
|
505
|
+
* The order is computed from the id rather than stored: `#12` must sort after `#2`, which a plain
|
|
506
|
+
* `ORDER BY id` gets wrong on every document past ten chunks because it compares text. So the
|
|
507
|
+
* trailing number is extracted and cast to `int`, and a bare id (a single-chunk document, which
|
|
508
|
+
* carries no `#n` suffix) collapses to `0` — the same rule `chunkIndexOf` applies in the other two
|
|
509
|
+
* adapters.
|
|
510
|
+
*
|
|
511
|
+
* `embedding` is left out of the projection: it is the widest column in the table and useless to a
|
|
512
|
+
* caller reading text back.
|
|
513
|
+
*/
|
|
514
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
377
515
|
/** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */
|
|
378
516
|
listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
|
|
379
517
|
/** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
|
|
@@ -463,6 +601,12 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
463
601
|
private readonly filterableFields;
|
|
464
602
|
private readonly lexicalScorer;
|
|
465
603
|
constructor(client: RedisSearchClient, options?: RedisVectorStoreOptions);
|
|
604
|
+
/**
|
|
605
|
+
* Telemetry self-description — the RediSearch INDEX, not the key prefix: the index is what both
|
|
606
|
+
* legs query, and two stores sharing one index (the dense and lexical halves of a hybrid) must
|
|
607
|
+
* report the same namespace or `describeSharedSource` would see them disagree and drop the store.
|
|
608
|
+
*/
|
|
609
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
466
610
|
/**
|
|
467
611
|
* Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —
|
|
468
612
|
* and if it DOES exist, compare what's there against this store's configuration instead of
|
|
@@ -550,6 +694,25 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
550
694
|
* document owns. The glob is escaped so `gone` does not reach into `goner#0`.
|
|
551
695
|
*/
|
|
552
696
|
private chunkKeys;
|
|
697
|
+
/**
|
|
698
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
699
|
+
*
|
|
700
|
+
* Reads the hashes directly (`HMGET text metadata_json`) rather than going through `FT.SEARCH`.
|
|
701
|
+
* Two reasons, and the second is the one that matters: the query language has no way to ask for
|
|
702
|
+
* "every chunk of this document" — `id` is the key name, not an indexed field — and `FT.SEARCH`
|
|
703
|
+
* caps at `LIMIT`, so paging a document through it would mean ordering by a field the index does
|
|
704
|
+
* not hold. `chunkKeys` is the same enumeration {@link RedisVectorStore.remove} and
|
|
705
|
+
* {@link RedisVectorStore.updateMetadata} already use, so "the chunks this returns" and "the chunks
|
|
706
|
+
* a delete would take" cannot drift apart.
|
|
707
|
+
*
|
|
708
|
+
* It inherits that method's cost too: one keyspace `SCAN` per call. Acceptable here because this
|
|
709
|
+
* is an inspection path (one document, on demand) and not something retrieval runs — but it is why
|
|
710
|
+
* this must not be put on a hot path or called in a loop over a corpus.
|
|
711
|
+
*
|
|
712
|
+
* `embedding` is deliberately not fetched: it is by far the largest field on the hash and nothing
|
|
713
|
+
* a caller reading text back can do with it.
|
|
714
|
+
*/
|
|
715
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
553
716
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
554
717
|
/**
|
|
555
718
|
* The id-only half of {@link RedisVectorStore.listDocuments}, served by `FT.SEARCH … NOCONTENT`:
|
|
@@ -653,6 +816,8 @@ declare class EmbeddingRetriever implements Retriever {
|
|
|
653
816
|
private readonly store;
|
|
654
817
|
private readonly options;
|
|
655
818
|
constructor(embedder: EmbeddingProvider, store: VectorStore, options?: EmbeddingRetrieverOptions);
|
|
819
|
+
/** Telemetry self-description — the dense strategy, over whatever store it was handed. */
|
|
820
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
656
821
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
657
822
|
}
|
|
658
823
|
|
|
@@ -712,6 +877,12 @@ declare class KeywordRetriever implements Retriever {
|
|
|
712
877
|
private readonly k1;
|
|
713
878
|
private readonly b;
|
|
714
879
|
constructor(options?: KeywordRetrieverOptions);
|
|
880
|
+
/**
|
|
881
|
+
* Telemetry self-description. No `store`: this retriever holds its own copy of the corpus in
|
|
882
|
+
* process, so attributing its retrievals to whichever vector store happens to hold the same chunks
|
|
883
|
+
* would credit a backend that was never queried.
|
|
884
|
+
*/
|
|
885
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
715
886
|
/** How many chunks are currently indexed. */
|
|
716
887
|
get size(): number;
|
|
717
888
|
/** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */
|
|
@@ -757,6 +928,8 @@ declare class KeywordRetriever implements Retriever {
|
|
|
757
928
|
declare class LexicalRetriever implements Retriever {
|
|
758
929
|
private readonly store;
|
|
759
930
|
constructor(store: LexicalVectorStore);
|
|
931
|
+
/** Telemetry self-description — the store's own lexical index, so the store is the same one. */
|
|
932
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
760
933
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
761
934
|
}
|
|
762
935
|
|
|
@@ -808,6 +981,8 @@ declare class HybridRetriever implements Retriever {
|
|
|
808
981
|
private readonly retrievers;
|
|
809
982
|
private readonly options;
|
|
810
983
|
constructor(retrievers: Retriever[], options?: HybridRetrieverOptions);
|
|
984
|
+
/** Telemetry self-description — the store only when every leg agrees; see `describeSharedSource`. */
|
|
985
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
811
986
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
812
987
|
}
|
|
813
988
|
|
|
@@ -826,6 +1001,8 @@ declare class RerankingRetriever implements Retriever {
|
|
|
826
1001
|
private readonly reranker;
|
|
827
1002
|
private readonly options;
|
|
828
1003
|
constructor(base: Retriever, reranker: Reranker, options?: RerankingRetrieverOptions);
|
|
1004
|
+
/** Telemetry self-description — the two-stage strategy, over the base's store. */
|
|
1005
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
829
1006
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
830
1007
|
}
|
|
831
1008
|
|
|
@@ -842,15 +1019,178 @@ declare class FilteredRetriever implements Retriever {
|
|
|
842
1019
|
private readonly base;
|
|
843
1020
|
private readonly filter;
|
|
844
1021
|
constructor(base: Retriever, filter: Record<string, unknown>);
|
|
1022
|
+
/** Telemetry self-description — this scope, over the base's store. */
|
|
1023
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
845
1024
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
846
1025
|
}
|
|
847
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
* Retrieval telemetry: one diagnostics event per retrieval, carrying how long it took, how much came
|
|
1029
|
+
* back, how good it was, and what served it.
|
|
1030
|
+
*
|
|
1031
|
+
* Until this existed, retrieval reached Telescope only as the *tool call* that wrapped it (see
|
|
1032
|
+
* `retrieval-tool.ts`), which says a retrieval happened and nothing else. The questions an operator
|
|
1033
|
+
* actually arrives with — is retrieval slow, is it returning nothing, is one collection colder than
|
|
1034
|
+
* the rest, did the scores collapse after a model swap — are all invisible in a tool-call row,
|
|
1035
|
+
* because a tool call that returns zero passages and one that returns five perfect ones are the same
|
|
1036
|
+
* row with the same `ok` status.
|
|
1037
|
+
*
|
|
1038
|
+
* ## One seam, not six
|
|
1039
|
+
*
|
|
1040
|
+
* Instrumentation is a WRAPPER over the core `Retriever` SPI ({@link instrumentRetriever}), not code
|
|
1041
|
+
* inside each retriever. Every retriever in this package — and every retriever outside it — is the
|
|
1042
|
+
* same one-method interface, so one wrapper covers all of them, including ones this package has
|
|
1043
|
+
* never heard of. The alternative, an `emit` inside each of the six `retrieve` methods, would have
|
|
1044
|
+
* had to be repeated in each, skipped by third-party retrievers entirely, and would have double- and
|
|
1045
|
+
* triple-counted the composed ones (a hybrid over two legs emitting three events for one retrieval).
|
|
1046
|
+
*
|
|
1047
|
+
* ## How a composed retrieval reports
|
|
1048
|
+
*
|
|
1049
|
+
* **The outermost instrumented retriever in a call tree emits; anything instrumented inside it stays
|
|
1050
|
+
* silent.** A retrieval is one question the caller asked, so it is one event no matter how many
|
|
1051
|
+
* retrievers cooperated to answer it: `RerankingRetriever` over a `HybridRetriever` over a dense and
|
|
1052
|
+
* a lexical leg is ONE retrieval, reported as `retriever: 'reranking'` with the duration of the
|
|
1053
|
+
* whole thing and the scores the caller actually got back.
|
|
1054
|
+
*
|
|
1055
|
+
* The suppression is enforced with an {@link AsyncLocalStorage} flag rather than by asking hosts to
|
|
1056
|
+
* wrap carefully, because the wrapping that double-counts is the natural one: a host that wraps its
|
|
1057
|
+
* top-level retriever and *also* wraps the dense leg it shares with another code path has not made a
|
|
1058
|
+
* mistake, and would otherwise get two events per retrieval, a zero-hit rate computed over a mixture
|
|
1059
|
+
* of legs and composites, and a retrieval count roughly twice its query count.
|
|
1060
|
+
*
|
|
1061
|
+
* ## Opt-out safety, and what the hot path pays
|
|
1062
|
+
*
|
|
1063
|
+
* The event goes out on `aviary:rag:retrieval` via `@dudousxd/nestjs-diagnostics`, whose `emit` is
|
|
1064
|
+
* gated on `channel.hasSubscribers` and never throws. This wrapper checks that same gate BEFORE it
|
|
1065
|
+
* does anything at all — no clock read, no `AsyncLocalStorage.run`, no score arithmetic — and
|
|
1066
|
+
* delegates straight through when nothing is listening. A host that wires this package without
|
|
1067
|
+
* Telescope (or any subscriber) pays two `Map` lookups and a boolean per retrieval, and nothing else
|
|
1068
|
+
* changes: the wrapper returns the base retriever's passages verbatim, and rethrows its errors
|
|
1069
|
+
* unchanged.
|
|
1070
|
+
*
|
|
1071
|
+
* ## Why this channel is not merged into the typed `ChannelRegistry`
|
|
1072
|
+
*
|
|
1073
|
+
* `@dudousxd/nestjs-agent-rag-media` already emits `media.ingested`/`media.removed`/`media.skipped`/
|
|
1074
|
+
* `media.failed` on the same `rag` lib, and flip's host app emits `rag:search` on it. Declaring
|
|
1075
|
+
* `ChannelRegistry['rag'] = { retrieval: RagRetrievalEvent }` here would narrow `EventOf<'rag'>` to
|
|
1076
|
+
* this one key process-wide and turn every one of those sibling emits into a compile error in any
|
|
1077
|
+
* project that installs both packages. The payload stays typed where it matters — {@link emitRagRetrieval}
|
|
1078
|
+
* takes {@link RagRetrievalEvent} — and the wire contract is documented below instead.
|
|
1079
|
+
*/
|
|
1080
|
+
/** The diagnostics lib segment. Shared with `@dudousxd/nestjs-agent-rag-media`'s `media.*` events. */
|
|
1081
|
+
declare const RAG_DIAGNOSTIC_LIB = "rag";
|
|
1082
|
+
/** The diagnostics event segment. */
|
|
1083
|
+
declare const RAG_RETRIEVAL_EVENT = "retrieval";
|
|
1084
|
+
/**
|
|
1085
|
+
* The full channel name a subscriber listens on: `aviary:rag:retrieval`. This is the wire contract —
|
|
1086
|
+
* `@dudousxd/nestjs-agent-telescope` subscribes to this exact string rather than importing this
|
|
1087
|
+
* package, which is what keeps the telescope extension free of a dependency on the retrieval stack
|
|
1088
|
+
* (and lets a host that uses neither still observe retrieval with a bare `diagnostics_channel`
|
|
1089
|
+
* subscription).
|
|
1090
|
+
*/
|
|
1091
|
+
declare const RAG_RETRIEVAL_CHANNEL: string;
|
|
1092
|
+
/**
|
|
1093
|
+
* The payload of one retrieval event. Counts, durations, kinds and scores only — never the query
|
|
1094
|
+
* text and never passage text. Both are user content, and this lands in an ops store with a short
|
|
1095
|
+
* retention window and a Slack-facing redaction budget; everything here is safe to put on a
|
|
1096
|
+
* dashboard and is what tells the failure modes apart anyway.
|
|
1097
|
+
*
|
|
1098
|
+
* The DURATION is not in here: it rides the envelope (`emit`'s `opts.durationMs`), which is the field
|
|
1099
|
+
* Telescope's OTel exporter turns into a latency histogram. Putting it in the payload instead would
|
|
1100
|
+
* have made retrieval latency a number somebody reads out of a JSON blob rather than a graph.
|
|
1101
|
+
*/
|
|
1102
|
+
interface RagRetrievalEvent {
|
|
1103
|
+
/** Which strategy answered — the OUTERMOST one, for a composed retriever. */
|
|
1104
|
+
retriever: RetrieverKind;
|
|
1105
|
+
/** Which backend held the chunks. Absent for an in-process retriever, or when legs disagree. */
|
|
1106
|
+
store?: VectorStoreKind;
|
|
1107
|
+
/** Store namespace (pg table / RediSearch index), or the host's logical collection. */
|
|
1108
|
+
collection?: string;
|
|
1109
|
+
/** What the caller asked for, so `chunks < topK` is readable as "the corpus ran out". */
|
|
1110
|
+
topK: number;
|
|
1111
|
+
/** How many passages came back. */
|
|
1112
|
+
chunks: number;
|
|
1113
|
+
/**
|
|
1114
|
+
* `chunks === 0`. Carried as its own flag rather than left to be derived, because the zero-hit RATE
|
|
1115
|
+
* is the headline number and deriving it means every consumer re-deriving the same predicate.
|
|
1116
|
+
*/
|
|
1117
|
+
zeroHit: boolean;
|
|
1118
|
+
/** Best score in the result, rounded to 4dp. Absent on a zero-hit. */
|
|
1119
|
+
topScore?: number;
|
|
1120
|
+
/** Mean score across the result, rounded to 4dp. Absent on a zero-hit. */
|
|
1121
|
+
meanScore?: number;
|
|
1122
|
+
/**
|
|
1123
|
+
* The retrieval threw. Reported rather than dropped because a failed retrieval and an empty one
|
|
1124
|
+
* look identical from the agent's side — both become "I couldn't find anything" — so a channel
|
|
1125
|
+
* that only carried successes would go quiet exactly when retrieval broke.
|
|
1126
|
+
*/
|
|
1127
|
+
failed: boolean;
|
|
1128
|
+
/** The error message, when `failed`. */
|
|
1129
|
+
error?: string;
|
|
1130
|
+
}
|
|
1131
|
+
/** Publish one retrieval event. `durationMs` rides the envelope; see {@link RagRetrievalEvent}. */
|
|
1132
|
+
declare function emitRagRetrieval(event: RagRetrievalEvent, durationMs: number): void;
|
|
1133
|
+
interface InstrumentRetrieverOptions {
|
|
1134
|
+
/**
|
|
1135
|
+
* Override the auto-detected {@link RetrievalDescriptor}, field by field. Needed for a retriever or
|
|
1136
|
+
* store from outside this package (which reports `retriever: 'unknown'` and no store), and for a
|
|
1137
|
+
* host that wants its own naming.
|
|
1138
|
+
*/
|
|
1139
|
+
describe?: RetrievalDescriptor;
|
|
1140
|
+
/**
|
|
1141
|
+
* The logical collection this retrieval searched, when the store namespace is not it. Pass a
|
|
1142
|
+
* function to read it off the call's metadata filter — the multi-tenant shape, where one index
|
|
1143
|
+
* holds every collection and the filter picks one:
|
|
1144
|
+
*
|
|
1145
|
+
* ```ts
|
|
1146
|
+
* instrumentRetriever(retriever, {
|
|
1147
|
+
* collection: (filter) => (typeof filter?.collectionId === 'string' ? filter.collectionId : undefined),
|
|
1148
|
+
* })
|
|
1149
|
+
* ```
|
|
1150
|
+
*
|
|
1151
|
+
* Returning `undefined` falls back to the store namespace, which is the right answer for a query
|
|
1152
|
+
* scoped to many collections at once: a breakdown panel needs ONE label per retrieval, and
|
|
1153
|
+
* inventing a synthetic `"a+b+c"` bucket would produce a chart whose slices are combinations
|
|
1154
|
+
* rather than collections.
|
|
1155
|
+
*/
|
|
1156
|
+
collection?: string | ((filter: Record<string, unknown> | undefined) => string | undefined);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Wrap a retriever so every `retrieve` publishes a {@link RagRetrievalEvent} on
|
|
1160
|
+
* {@link RAG_RETRIEVAL_CHANNEL}. Wrap the retriever you actually hand to the agent — the outermost
|
|
1161
|
+
* one — and nothing else needs changing:
|
|
1162
|
+
*
|
|
1163
|
+
* ```ts
|
|
1164
|
+
* const retriever = instrumentRetriever(
|
|
1165
|
+
* new HybridRetriever([new EmbeddingRetriever(embedder, store), new LexicalRetriever(store)]),
|
|
1166
|
+
* );
|
|
1167
|
+
* ```
|
|
1168
|
+
*
|
|
1169
|
+
* {@link createRetrievalTool} already does this for the tool it builds, so agentic retrieval is
|
|
1170
|
+
* instrumented with no host change at all; call this directly for inject-mode retrieval
|
|
1171
|
+
* (`AgentModule.forRoot({ retrieval })`) or for a host that calls `retrieve` itself. Wrapping twice
|
|
1172
|
+
* is harmless — see the composed-retrieval section above.
|
|
1173
|
+
*/
|
|
1174
|
+
declare function instrumentRetriever(retriever: Retriever, options?: InstrumentRetrieverOptions): Retriever;
|
|
1175
|
+
|
|
848
1176
|
interface RetrievalToolOptions {
|
|
849
1177
|
/** Tool name the model sees. Default `search_knowledge`. */
|
|
850
1178
|
name?: string;
|
|
851
1179
|
description?: string;
|
|
852
1180
|
/** Passages per search. Default lets the retriever decide (typically 5). */
|
|
853
1181
|
topK?: number;
|
|
1182
|
+
/**
|
|
1183
|
+
* Retrieval telemetry (`aviary:rag:retrieval` — duration, chunk count, score distribution, store
|
|
1184
|
+
* and collection). **On by default**, because this tool is the once-per-retrieval seam in agentic
|
|
1185
|
+
* mode: instrumenting here is what makes the RAG panels work for a host that only ever calls
|
|
1186
|
+
* `createRetrievalTool`, with no wiring at all. It costs nothing when nobody subscribes (see
|
|
1187
|
+
* {@link instrumentRetriever}), and cannot double-count when the host has already wrapped the
|
|
1188
|
+
* retriever itself.
|
|
1189
|
+
*
|
|
1190
|
+
* Pass `false` to opt a single tool out — a host that reports retrieval its own way, or one whose
|
|
1191
|
+
* corpus is small enough that the events are noise.
|
|
1192
|
+
*/
|
|
1193
|
+
telemetry?: boolean | InstrumentRetrieverOptions;
|
|
854
1194
|
}
|
|
855
1195
|
/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */
|
|
856
1196
|
interface RetrievalTool {
|
|
@@ -862,10 +1202,14 @@ interface RetrievalTool {
|
|
|
862
1202
|
* decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources
|
|
863
1203
|
* flow to the frontend and telescope through the normal tool-call machinery (the citation surface).
|
|
864
1204
|
*
|
|
1205
|
+
* The retrieval ITSELF is also instrumented (`aviary:rag:retrieval`) unless `telemetry: false` — the
|
|
1206
|
+
* tool-call row says a retrieval happened, the retrieval event says how long it took, how much came
|
|
1207
|
+
* back and how good it was. See {@link instrumentRetriever}.
|
|
1208
|
+
*
|
|
865
1209
|
* ```ts
|
|
866
1210
|
* providers: [provideAgentTool(createRetrievalTool(retriever))]
|
|
867
1211
|
* ```
|
|
868
1212
|
*/
|
|
869
1213
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
870
1214
|
|
|
871
|
-
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 };
|
|
1215
|
+
export { type ChunkOptions, type ChunkRecord, type DescribesRetrieval, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagRetrievalEvent, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, documentIdOf, emitRagRetrieval, ingestChunks, ingestDocuments, instrumentRetriever, isLexicalVectorStore };
|