@dudousxd/nestjs-agent-rag 0.9.0 → 0.10.1
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 +60 -2
- package/dist/index.cjs +221 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -1
- package/dist/index.d.ts +258 -1
- package/dist/index.js +212 -1
- package/dist/index.js.map +1 -1
- package/package.json +13 -3
package/dist/index.d.ts
CHANGED
|
@@ -319,6 +319,68 @@ declare function chunkIndexOf(chunkId: string): number;
|
|
|
319
319
|
/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */
|
|
320
320
|
declare function documentIdOf(chunkId: string): string;
|
|
321
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
|
+
|
|
322
384
|
/**
|
|
323
385
|
* An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter
|
|
324
386
|
* for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).
|
|
@@ -331,6 +393,12 @@ declare function documentIdOf(chunkId: string): string;
|
|
|
331
393
|
*/
|
|
332
394
|
declare class MemoryVectorStore implements VectorStore {
|
|
333
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;
|
|
334
402
|
upsert(records: VectorRecord[]): Promise<void>;
|
|
335
403
|
remove(documentId: string): Promise<void>;
|
|
336
404
|
/**
|
|
@@ -411,6 +479,8 @@ declare class PgVectorStore implements VectorStore {
|
|
|
411
479
|
private readonly table;
|
|
412
480
|
private readonly dimensions;
|
|
413
481
|
constructor(client: PgClient, options?: PgVectorStoreOptions);
|
|
482
|
+
/** Telemetry self-description — the table is the namespace a retrieval was served from. */
|
|
483
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
414
484
|
/** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
|
|
415
485
|
ensureSchema(): Promise<void>;
|
|
416
486
|
upsert(records: VectorRecord[]): Promise<void>;
|
|
@@ -531,6 +601,12 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
531
601
|
private readonly filterableFields;
|
|
532
602
|
private readonly lexicalScorer;
|
|
533
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;
|
|
534
610
|
/**
|
|
535
611
|
* Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —
|
|
536
612
|
* and if it DOES exist, compare what's there against this store's configuration instead of
|
|
@@ -740,6 +816,8 @@ declare class EmbeddingRetriever implements Retriever {
|
|
|
740
816
|
private readonly store;
|
|
741
817
|
private readonly options;
|
|
742
818
|
constructor(embedder: EmbeddingProvider, store: VectorStore, options?: EmbeddingRetrieverOptions);
|
|
819
|
+
/** Telemetry self-description — the dense strategy, over whatever store it was handed. */
|
|
820
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
743
821
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
744
822
|
}
|
|
745
823
|
|
|
@@ -799,6 +877,12 @@ declare class KeywordRetriever implements Retriever {
|
|
|
799
877
|
private readonly k1;
|
|
800
878
|
private readonly b;
|
|
801
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;
|
|
802
886
|
/** How many chunks are currently indexed. */
|
|
803
887
|
get size(): number;
|
|
804
888
|
/** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */
|
|
@@ -844,6 +928,8 @@ declare class KeywordRetriever implements Retriever {
|
|
|
844
928
|
declare class LexicalRetriever implements Retriever {
|
|
845
929
|
private readonly store;
|
|
846
930
|
constructor(store: LexicalVectorStore);
|
|
931
|
+
/** Telemetry self-description — the store's own lexical index, so the store is the same one. */
|
|
932
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
847
933
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
848
934
|
}
|
|
849
935
|
|
|
@@ -895,6 +981,8 @@ declare class HybridRetriever implements Retriever {
|
|
|
895
981
|
private readonly retrievers;
|
|
896
982
|
private readonly options;
|
|
897
983
|
constructor(retrievers: Retriever[], options?: HybridRetrieverOptions);
|
|
984
|
+
/** Telemetry self-description — the store only when every leg agrees; see `describeSharedSource`. */
|
|
985
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
898
986
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
899
987
|
}
|
|
900
988
|
|
|
@@ -913,6 +1001,8 @@ declare class RerankingRetriever implements Retriever {
|
|
|
913
1001
|
private readonly reranker;
|
|
914
1002
|
private readonly options;
|
|
915
1003
|
constructor(base: Retriever, reranker: Reranker, options?: RerankingRetrieverOptions);
|
|
1004
|
+
/** Telemetry self-description — the two-stage strategy, over the base's store. */
|
|
1005
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
916
1006
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
917
1007
|
}
|
|
918
1008
|
|
|
@@ -929,15 +1019,178 @@ declare class FilteredRetriever implements Retriever {
|
|
|
929
1019
|
private readonly base;
|
|
930
1020
|
private readonly filter;
|
|
931
1021
|
constructor(base: Retriever, filter: Record<string, unknown>);
|
|
1022
|
+
/** Telemetry self-description — this scope, over the base's store. */
|
|
1023
|
+
describeRetrieval(): RetrievalDescriptor;
|
|
932
1024
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
933
1025
|
}
|
|
934
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
|
+
|
|
935
1176
|
interface RetrievalToolOptions {
|
|
936
1177
|
/** Tool name the model sees. Default `search_knowledge`. */
|
|
937
1178
|
name?: string;
|
|
938
1179
|
description?: string;
|
|
939
1180
|
/** Passages per search. Default lets the retriever decide (typically 5). */
|
|
940
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;
|
|
941
1194
|
}
|
|
942
1195
|
/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */
|
|
943
1196
|
interface RetrievalTool {
|
|
@@ -949,10 +1202,14 @@ interface RetrievalTool {
|
|
|
949
1202
|
* decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources
|
|
950
1203
|
* flow to the frontend and telescope through the normal tool-call machinery (the citation surface).
|
|
951
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
|
+
*
|
|
952
1209
|
* ```ts
|
|
953
1210
|
* providers: [provideAgentTool(createRetrievalTool(retriever))]
|
|
954
1211
|
* ```
|
|
955
1212
|
*/
|
|
956
1213
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
957
1214
|
|
|
958
|
-
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, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkIndexOf, 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 };
|
package/dist/index.js
CHANGED
|
@@ -165,6 +165,16 @@ var MemoryVectorStore = class {
|
|
|
165
165
|
__name(this, "MemoryVectorStore");
|
|
166
166
|
}
|
|
167
167
|
records = /* @__PURE__ */ new Map();
|
|
168
|
+
/**
|
|
169
|
+
* Telemetry self-description. No `collection`: there is no namespace to name — the corpus IS this
|
|
170
|
+
* instance, and two `MemoryVectorStore`s in one process are two corpora with nothing to tell them
|
|
171
|
+
* apart on a dashboard.
|
|
172
|
+
*/
|
|
173
|
+
describeRetrieval() {
|
|
174
|
+
return {
|
|
175
|
+
store: "memory"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
168
178
|
async upsert(records) {
|
|
169
179
|
for (const record of records) {
|
|
170
180
|
this.records.set(record.id, record);
|
|
@@ -359,6 +369,13 @@ var PgVectorStore = class {
|
|
|
359
369
|
this.table = options.table ?? "agent_rag_chunks";
|
|
360
370
|
this.dimensions = options.dimensions ?? 1536;
|
|
361
371
|
}
|
|
372
|
+
/** Telemetry self-description — the table is the namespace a retrieval was served from. */
|
|
373
|
+
describeRetrieval() {
|
|
374
|
+
return {
|
|
375
|
+
store: "pg",
|
|
376
|
+
collection: this.table
|
|
377
|
+
};
|
|
378
|
+
}
|
|
362
379
|
/** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
|
|
363
380
|
async ensureSchema() {
|
|
364
381
|
await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
@@ -621,6 +638,17 @@ var RedisVectorStore = class {
|
|
|
621
638
|
this.lexicalScorer = options.lexicalScorer ?? "BM25";
|
|
622
639
|
}
|
|
623
640
|
/**
|
|
641
|
+
* Telemetry self-description — the RediSearch INDEX, not the key prefix: the index is what both
|
|
642
|
+
* legs query, and two stores sharing one index (the dense and lexical halves of a hybrid) must
|
|
643
|
+
* report the same namespace or `describeSharedSource` would see them disagree and drop the store.
|
|
644
|
+
*/
|
|
645
|
+
describeRetrieval() {
|
|
646
|
+
return {
|
|
647
|
+
store: "redis",
|
|
648
|
+
collection: this.index
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
624
652
|
* Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —
|
|
625
653
|
* and if it DOES exist, compare what's there against this store's configuration instead of
|
|
626
654
|
* assuming they agree. An index that outlives a config change is the normal case (schemas live in
|
|
@@ -1449,6 +1477,31 @@ function parseMetadata(raw) {
|
|
|
1449
1477
|
}
|
|
1450
1478
|
__name(parseMetadata, "parseMetadata");
|
|
1451
1479
|
|
|
1480
|
+
// src/retrieval-descriptor.ts
|
|
1481
|
+
function describesRetrieval(value) {
|
|
1482
|
+
return typeof value === "object" && value !== null && typeof Reflect.get(value, "describeRetrieval") === "function";
|
|
1483
|
+
}
|
|
1484
|
+
__name(describesRetrieval, "describesRetrieval");
|
|
1485
|
+
function describeRetrieval(candidate) {
|
|
1486
|
+
return describesRetrieval(candidate) ? candidate.describeRetrieval() : {};
|
|
1487
|
+
}
|
|
1488
|
+
__name(describeRetrieval, "describeRetrieval");
|
|
1489
|
+
function describeSource(base) {
|
|
1490
|
+
const { retriever: _ignored, ...source } = describeRetrieval(base);
|
|
1491
|
+
return source;
|
|
1492
|
+
}
|
|
1493
|
+
__name(describeSource, "describeSource");
|
|
1494
|
+
function describeSharedSource(legs) {
|
|
1495
|
+
const sources = legs.map(describeSource);
|
|
1496
|
+
const [first] = sources;
|
|
1497
|
+
if (first === void 0) {
|
|
1498
|
+
return {};
|
|
1499
|
+
}
|
|
1500
|
+
const agrees = sources.every((source) => source.store === first.store && source.collection === first.collection);
|
|
1501
|
+
return agrees ? first : {};
|
|
1502
|
+
}
|
|
1503
|
+
__name(describeSharedSource, "describeSharedSource");
|
|
1504
|
+
|
|
1452
1505
|
// src/embedding-retriever.ts
|
|
1453
1506
|
var EmbeddingRetriever = class {
|
|
1454
1507
|
static {
|
|
@@ -1462,6 +1515,13 @@ var EmbeddingRetriever = class {
|
|
|
1462
1515
|
this.store = store;
|
|
1463
1516
|
this.options = options;
|
|
1464
1517
|
}
|
|
1518
|
+
/** Telemetry self-description — the dense strategy, over whatever store it was handed. */
|
|
1519
|
+
describeRetrieval() {
|
|
1520
|
+
return {
|
|
1521
|
+
retriever: "embedding",
|
|
1522
|
+
...describeSource(this.store)
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1465
1525
|
async retrieve(query, options = {}) {
|
|
1466
1526
|
const [embedding] = await this.embedder.embed([
|
|
1467
1527
|
query
|
|
@@ -1494,6 +1554,16 @@ var KeywordRetriever = class {
|
|
|
1494
1554
|
this.k1 = options.k1 ?? 1.5;
|
|
1495
1555
|
this.b = options.b ?? 0.75;
|
|
1496
1556
|
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Telemetry self-description. No `store`: this retriever holds its own copy of the corpus in
|
|
1559
|
+
* process, so attributing its retrievals to whichever vector store happens to hold the same chunks
|
|
1560
|
+
* would credit a backend that was never queried.
|
|
1561
|
+
*/
|
|
1562
|
+
describeRetrieval() {
|
|
1563
|
+
return {
|
|
1564
|
+
retriever: "keyword"
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1497
1567
|
/** How many chunks are currently indexed. */
|
|
1498
1568
|
get size() {
|
|
1499
1569
|
return this.docs.size;
|
|
@@ -1611,6 +1681,13 @@ var LexicalRetriever = class {
|
|
|
1611
1681
|
constructor(store) {
|
|
1612
1682
|
this.store = store;
|
|
1613
1683
|
}
|
|
1684
|
+
/** Telemetry self-description — the store's own lexical index, so the store is the same one. */
|
|
1685
|
+
describeRetrieval() {
|
|
1686
|
+
return {
|
|
1687
|
+
retriever: "lexical",
|
|
1688
|
+
...describeSource(this.store)
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1614
1691
|
async retrieve(query, options = {}) {
|
|
1615
1692
|
return this.store.searchText(query, {
|
|
1616
1693
|
topK: options.topK ?? 5,
|
|
@@ -1632,6 +1709,13 @@ var HybridRetriever = class {
|
|
|
1632
1709
|
this.retrievers = retrievers;
|
|
1633
1710
|
this.options = options;
|
|
1634
1711
|
}
|
|
1712
|
+
/** Telemetry self-description — the store only when every leg agrees; see `describeSharedSource`. */
|
|
1713
|
+
describeRetrieval() {
|
|
1714
|
+
return {
|
|
1715
|
+
retriever: "hybrid",
|
|
1716
|
+
...describeSharedSource(this.retrievers)
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1635
1719
|
async retrieve(query, options = {}) {
|
|
1636
1720
|
const k = this.options.k ?? 60;
|
|
1637
1721
|
const fetchTopK = this.options.fetchTopK ?? 20;
|
|
@@ -1680,6 +1764,13 @@ var RerankingRetriever = class {
|
|
|
1680
1764
|
this.reranker = reranker;
|
|
1681
1765
|
this.options = options;
|
|
1682
1766
|
}
|
|
1767
|
+
/** Telemetry self-description — the two-stage strategy, over the base's store. */
|
|
1768
|
+
describeRetrieval() {
|
|
1769
|
+
return {
|
|
1770
|
+
retriever: "reranking",
|
|
1771
|
+
...describeSource(this.base)
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1683
1774
|
async retrieve(query, options = {}) {
|
|
1684
1775
|
const candidates = await this.base.retrieve(query, {
|
|
1685
1776
|
topK: this.options.fetchTopK ?? 20,
|
|
@@ -1704,6 +1795,13 @@ var FilteredRetriever = class {
|
|
|
1704
1795
|
this.base = base;
|
|
1705
1796
|
this.filter = filter;
|
|
1706
1797
|
}
|
|
1798
|
+
/** Telemetry self-description — this scope, over the base's store. */
|
|
1799
|
+
describeRetrieval() {
|
|
1800
|
+
return {
|
|
1801
|
+
retriever: "filtered",
|
|
1802
|
+
...describeSource(this.base)
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1707
1805
|
async retrieve(query, options = {}) {
|
|
1708
1806
|
return this.base.retrieve(query, {
|
|
1709
1807
|
...options,
|
|
@@ -1753,6 +1851,108 @@ async function ingestDocuments(documents, options) {
|
|
|
1753
1851
|
}
|
|
1754
1852
|
__name(ingestDocuments, "ingestDocuments");
|
|
1755
1853
|
|
|
1854
|
+
// src/retrieval-telemetry.ts
|
|
1855
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1856
|
+
import { channelName, emit, getChannel } from "@dudousxd/nestjs-diagnostics";
|
|
1857
|
+
var RAG_DIAGNOSTIC_LIB = "rag";
|
|
1858
|
+
var RAG_RETRIEVAL_EVENT = "retrieval";
|
|
1859
|
+
var RAG_RETRIEVAL_CHANNEL = channelName(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT);
|
|
1860
|
+
function emitRagRetrieval(event, durationMs) {
|
|
1861
|
+
emit(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT, event, {
|
|
1862
|
+
durationMs
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
__name(emitRagRetrieval, "emitRagRetrieval");
|
|
1866
|
+
var DEFAULT_TOP_K = 5;
|
|
1867
|
+
var inRetrieval = new AsyncLocalStorage();
|
|
1868
|
+
function round4(value) {
|
|
1869
|
+
return Math.round(value * 1e4) / 1e4;
|
|
1870
|
+
}
|
|
1871
|
+
__name(round4, "round4");
|
|
1872
|
+
function scoreStats(passages) {
|
|
1873
|
+
const scores = passages.map((passage) => passage.score).filter((score) => Number.isFinite(score));
|
|
1874
|
+
if (scores.length === 0) {
|
|
1875
|
+
return {};
|
|
1876
|
+
}
|
|
1877
|
+
const sum = scores.reduce((total, score) => total + score, 0);
|
|
1878
|
+
return {
|
|
1879
|
+
topScore: round4(Math.max(...scores)),
|
|
1880
|
+
meanScore: round4(sum / scores.length)
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
__name(scoreStats, "scoreStats");
|
|
1884
|
+
var InstrumentedRetriever = class InstrumentedRetriever2 {
|
|
1885
|
+
static {
|
|
1886
|
+
__name(this, "InstrumentedRetriever");
|
|
1887
|
+
}
|
|
1888
|
+
base;
|
|
1889
|
+
options;
|
|
1890
|
+
constructor(base, options = {}) {
|
|
1891
|
+
this.base = base;
|
|
1892
|
+
this.options = options;
|
|
1893
|
+
}
|
|
1894
|
+
/** Transparent to detection: wrapping a retriever must not turn it into an `unknown` one. */
|
|
1895
|
+
describeRetrieval() {
|
|
1896
|
+
return {
|
|
1897
|
+
...describeRetrieval(this.base),
|
|
1898
|
+
...this.options.describe
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
async retrieve(query, options = {}) {
|
|
1902
|
+
if (!getChannel(RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_EVENT).hasSubscribers) {
|
|
1903
|
+
return this.base.retrieve(query, options);
|
|
1904
|
+
}
|
|
1905
|
+
if (inRetrieval.getStore() === true) {
|
|
1906
|
+
return this.base.retrieve(query, options);
|
|
1907
|
+
}
|
|
1908
|
+
const descriptor = this.describeRetrieval();
|
|
1909
|
+
const topK = options.topK ?? DEFAULT_TOP_K;
|
|
1910
|
+
const startedAt = Date.now();
|
|
1911
|
+
try {
|
|
1912
|
+
const passages = await inRetrieval.run(true, () => this.base.retrieve(query, options));
|
|
1913
|
+
emitRagRetrieval({
|
|
1914
|
+
retriever: descriptor.retriever ?? "unknown",
|
|
1915
|
+
...descriptor.store !== void 0 ? {
|
|
1916
|
+
store: descriptor.store
|
|
1917
|
+
} : {},
|
|
1918
|
+
...this.collectionOf(options.filter, descriptor),
|
|
1919
|
+
topK,
|
|
1920
|
+
chunks: passages.length,
|
|
1921
|
+
zeroHit: passages.length === 0,
|
|
1922
|
+
...scoreStats(passages),
|
|
1923
|
+
failed: false
|
|
1924
|
+
}, Date.now() - startedAt);
|
|
1925
|
+
return passages;
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
emitRagRetrieval({
|
|
1928
|
+
retriever: descriptor.retriever ?? "unknown",
|
|
1929
|
+
...descriptor.store !== void 0 ? {
|
|
1930
|
+
store: descriptor.store
|
|
1931
|
+
} : {},
|
|
1932
|
+
...this.collectionOf(options.filter, descriptor),
|
|
1933
|
+
topK,
|
|
1934
|
+
chunks: 0,
|
|
1935
|
+
zeroHit: true,
|
|
1936
|
+
failed: true,
|
|
1937
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1938
|
+
}, Date.now() - startedAt);
|
|
1939
|
+
throw error;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
/** The host's collection for this call, else the store namespace, else nothing. */
|
|
1943
|
+
collectionOf(filter, descriptor) {
|
|
1944
|
+
const { collection } = this.options;
|
|
1945
|
+
const resolved = typeof collection === "function" ? collection(filter) : collection ?? descriptor.collection;
|
|
1946
|
+
return resolved !== void 0 ? {
|
|
1947
|
+
collection: resolved
|
|
1948
|
+
} : {};
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
function instrumentRetriever(retriever, options = {}) {
|
|
1952
|
+
return new InstrumentedRetriever(retriever, options);
|
|
1953
|
+
}
|
|
1954
|
+
__name(instrumentRetriever, "instrumentRetriever");
|
|
1955
|
+
|
|
1756
1956
|
// src/retrieval-tool.ts
|
|
1757
1957
|
function isQueryInput(value) {
|
|
1758
1958
|
return typeof value === "object" && value !== null && "query" in value && typeof value.query === "string";
|
|
@@ -1779,6 +1979,8 @@ var querySchema = {
|
|
|
1779
1979
|
};
|
|
1780
1980
|
function createRetrievalTool(retriever, options = {}) {
|
|
1781
1981
|
const topK = options.topK;
|
|
1982
|
+
const { telemetry = true } = options;
|
|
1983
|
+
const instrumented = telemetry === false ? retriever : instrumentRetriever(retriever, telemetry === true ? {} : telemetry);
|
|
1782
1984
|
return {
|
|
1783
1985
|
spec: {
|
|
1784
1986
|
name: options.name ?? "search_knowledge",
|
|
@@ -1791,7 +1993,7 @@ function createRetrievalTool(retriever, options = {}) {
|
|
|
1791
1993
|
if (!isQueryInput(input)) {
|
|
1792
1994
|
throw new Error("retrieval tool expects { query: string }");
|
|
1793
1995
|
}
|
|
1794
|
-
const passages = await
|
|
1996
|
+
const passages = await instrumented.retrieve(input.query, topK !== void 0 ? {
|
|
1795
1997
|
topK
|
|
1796
1998
|
} : {});
|
|
1797
1999
|
return {
|
|
@@ -1810,6 +2012,9 @@ export {
|
|
|
1810
2012
|
LexicalRetriever,
|
|
1811
2013
|
MemoryVectorStore,
|
|
1812
2014
|
PgVectorStore,
|
|
2015
|
+
RAG_DIAGNOSTIC_LIB,
|
|
2016
|
+
RAG_RETRIEVAL_CHANNEL,
|
|
2017
|
+
RAG_RETRIEVAL_EVENT,
|
|
1813
2018
|
RedisVectorSchemaMismatchError,
|
|
1814
2019
|
RedisVectorStore,
|
|
1815
2020
|
RerankingRetriever,
|
|
@@ -1819,9 +2024,15 @@ export {
|
|
|
1819
2024
|
chunkIndexOf,
|
|
1820
2025
|
chunkText,
|
|
1821
2026
|
createRetrievalTool,
|
|
2027
|
+
describeRetrieval,
|
|
2028
|
+
describeSharedSource,
|
|
2029
|
+
describeSource,
|
|
2030
|
+
describesRetrieval,
|
|
1822
2031
|
documentIdOf,
|
|
2032
|
+
emitRagRetrieval,
|
|
1823
2033
|
ingestChunks,
|
|
1824
2034
|
ingestDocuments,
|
|
2035
|
+
instrumentRetriever,
|
|
1825
2036
|
isLexicalVectorStore
|
|
1826
2037
|
};
|
|
1827
2038
|
//# sourceMappingURL=index.js.map
|