@dudousxd/nestjs-agent-rag 0.13.0 → 0.14.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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Passage, Retriever, EmbeddingProvider, RetrieveOptions, Reranker, ToolSpec, ToolHandler, RerankOptions, ModelProvider, AiToolCtx } from '@dudousxd/nestjs-agent-core';
1
+ import { Passage, Retriever, EmbeddingProvider, RetrieveOptions, Reranker, ToolSpec, ToolHandler, AiToolCtx, RerankOptions, ModelProvider } from '@dudousxd/nestjs-agent-core';
2
2
 
3
3
  interface ChunkOptions {
4
4
  /** Target max characters per chunk. Default 800. */
@@ -89,7 +89,10 @@ interface VectorRecord {
89
89
  }
90
90
  interface VectorSearchOptions {
91
91
  topK: number;
92
- /** Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`. */
92
+ /**
93
+ * Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`.
94
+ * An empty array or an `undefined` value matches nothing.
95
+ */
93
96
  filter?: Record<string, unknown>;
94
97
  }
95
98
  /**
@@ -235,6 +238,8 @@ interface VectorStore {
235
238
  * got built wrong (a dropped key, an `undefined` scope) than a deliberate request to delete
236
239
  * everything. Deliberate mass deletion stays available and stays explicit:
237
240
  * `removeMany(await store.listDocumentIds())`, or the backend's own drop.
241
+ * - **A key with an `undefined` value throws** {@link UnsafeRemovalError} too, for the same reason:
242
+ * `removeWhere({ tenantId: actor.tenantRef })` with no tenant is a bug, not a request.
238
243
  *
239
244
  * What it removes is exactly what a {@link VectorStore.search} carrying the same filter could
240
245
  * reach — no more (it cannot escape the filter) and no less.
@@ -273,6 +278,16 @@ interface LexicalVectorStore extends VectorStore {
273
278
  */
274
279
  searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
275
280
  }
281
+ /**
282
+ * An **optional** capability: swap all of a document's chunks atomically, so no search or crash ever
283
+ * sees it half-replaced. Optional because atomicity is exactly what not every backend can promise over
284
+ * its client; {@link replaceDocumentChunks} falls back to `remove` plus `upsert` for the others.
285
+ */
286
+ interface AtomicReplaceVectorStore extends VectorStore {
287
+ replaceDocument(documentId: string, records: VectorRecord[]): Promise<void>;
288
+ }
289
+ /** Replaces a document's chunks, atomically where the store can. */
290
+ declare function replaceDocumentChunks(store: VectorStore, documentId: string, records: VectorRecord[]): Promise<void>;
276
291
  /**
277
292
  * Does this store carry the optional {@link LexicalVectorStore} capability? Lets a consumer pick the
278
293
  * store-backed lexical leg when available and fall back to `KeywordRetriever` (or to dense-only)
@@ -287,10 +302,10 @@ declare function isLexicalVectorStore(store: VectorStore): store is LexicalVecto
287
302
  */
288
303
  declare class UnsafeRemovalError extends Error {
289
304
  /** Why the removal was refused. */
290
- readonly reason: 'empty-filter' | 'unindexed-field';
305
+ readonly reason: 'empty-filter' | 'unindexed-field' | 'undefined-value';
291
306
  constructor(
292
307
  /** Why the removal was refused. */
293
- reason: 'empty-filter' | 'unindexed-field', message: string);
308
+ reason: 'empty-filter' | 'unindexed-field' | 'undefined-value', message: string);
294
309
  }
295
310
  /** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */
296
311
  interface IndexedDocument {
@@ -562,6 +577,13 @@ interface PgVectorStoreOptions {
562
577
  * the chunk metadata) when two models share a width.
563
578
  */
564
579
  dimensions?: number | readonly number[];
580
+ /**
581
+ * The pgvector column type. `halfvec` stores each dimension in 2 bytes instead of 4, and its HNSW
582
+ * index accepts up to 4000 dimensions where `vector` stops at 2000 (so `text-embedding-3-large`'s
583
+ * 3072 needs it). Needs a single width, not a list. Only affects the DDL for a new table and the
584
+ * casts in statements: converting an existing table is a migration of yours. Default `vector`.
585
+ */
586
+ vectorType?: 'vector' | 'halfvec';
565
587
  /**
566
588
  * Allow chunks with no embedding (`embedding: []` on upsert is stored as `NULL`): index text first
567
589
  * and embed later, or keep full-text-only chunks when there is no embedding model at all. The
@@ -617,13 +639,14 @@ interface PgVectorStoreOptions {
617
639
  * Subclassing: every statement filters through {@link PgVectorStore.whereConditions}, so a subclass
618
640
  * can map an opaque filter onto its own columns (a tenant id, an ACL) in one place.
619
641
  */
620
- declare class PgVectorStore implements VectorStore {
642
+ declare class PgVectorStore implements AtomicReplaceVectorStore {
621
643
  protected readonly client: PgClient;
622
644
  protected readonly table: string;
623
645
  /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */
624
646
  private readonly dimensions;
625
647
  /** The widths with a partial HNSW index, for a mixed-dimension table. */
626
648
  private readonly indexedDimensions;
649
+ private readonly vectorType;
627
650
  private readonly nullableEmbeddings;
628
651
  private readonly iterativeScan;
629
652
  private readonly efSearch;
@@ -645,6 +668,12 @@ declare class PgVectorStore implements VectorStore {
645
668
  * {@link PgVectorStoreOptions.nullableEmbeddings}).
646
669
  */
647
670
  upsert(records: VectorRecord[]): Promise<void>;
671
+ /**
672
+ * Swaps a document's chunks in one transaction, so a crash can't lose it and a search sees the old
673
+ * version or the new one. A client without `transaction` falls back to two statements.
674
+ */
675
+ replaceDocument(documentId: string, records: VectorRecord[]): Promise<void>;
676
+ private upsertWith;
648
677
  remove(documentId: string): Promise<void>;
649
678
  /**
650
679
  * Rewrite a document's metadata without re-embedding it. See {@link VectorStore.updateMetadata}
@@ -1394,6 +1423,11 @@ interface RetrievalToolOptions {
1394
1423
  description?: string;
1395
1424
  /** Passages per search. Default lets the retriever decide (typically 5). */
1396
1425
  topK?: number;
1426
+ /**
1427
+ * The access filter for this call, from the tool context — e.g.
1428
+ * `(ctx) => ({ tenant: ctx.actor.tenantRef })`. Without it, every actor searches the same corpus.
1429
+ */
1430
+ filter?: (ctx: AiToolCtx) => Record<string, unknown> | undefined;
1397
1431
  /**
1398
1432
  * Retrieval telemetry (`aviary:rag:retrieval` — duration, chunk count, score distribution, store
1399
1433
  * and collection). **On by default**, because this tool is the once-per-retrieval seam in agentic
@@ -1427,6 +1461,23 @@ interface RetrievalTool {
1427
1461
  */
1428
1462
  declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
1429
1463
 
1464
+ /** What one embedding call consumed: the whole batch, not per text. */
1465
+ interface EmbeddingUsage {
1466
+ inputTokens: number;
1467
+ /** The model the provider reported, needed to estimate a cost. */
1468
+ modelId?: string;
1469
+ }
1470
+ interface EmbeddingResult {
1471
+ vectors: number[][];
1472
+ usage?: EmbeddingUsage;
1473
+ }
1474
+ /** An {@link EmbeddingProvider} that can also say what a call cost. `openAiEmbeddings` is one. */
1475
+ interface UsageReportingEmbeddingProvider extends EmbeddingProvider {
1476
+ embedWithUsage(texts: string[]): Promise<EmbeddingResult>;
1477
+ }
1478
+ /** Calls `embedWithUsage` when the provider has it, otherwise `embed` with no usage. */
1479
+ declare function embedCountingUsage(provider: EmbeddingProvider, texts: string[]): Promise<EmbeddingResult>;
1480
+
1430
1481
  /** A non-2xx answer (or a malformed one) from an embeddings or rerank endpoint. */
1431
1482
  declare class HttpModelError extends Error {
1432
1483
  /** HTTP status; `502` when the endpoint answered 2xx with a body that doesn't fit the contract. */
@@ -1497,7 +1548,7 @@ declare function isBatchTooLarge(error: unknown): boolean;
1497
1548
  * const local = openAiEmbeddings({ baseUrl: 'http://tei:8080/v1', model: 'BAAI/bge-m3' });
1498
1549
  * ```
1499
1550
  */
1500
- declare function openAiEmbeddings(options: OpenAiEmbeddingsOptions): EmbeddingProvider;
1551
+ declare function openAiEmbeddings(options: OpenAiEmbeddingsOptions): UsageReportingEmbeddingProvider;
1501
1552
 
1502
1553
  interface HttpRerankerOptions {
1503
1554
  /**
@@ -1543,6 +1594,136 @@ declare class HttpReranker implements Reranker {
1543
1594
  rerank(query: string, passages: Passage[], options?: RerankOptions): Promise<Passage[]>;
1544
1595
  }
1545
1596
 
1597
+ /** The metadata key separating models that share one store. Reserved: it always wins over yours. */
1598
+ declare const RAG_ENTITY_KEY = "ragEntity";
1599
+ /** What a scope value can be. Compared as strings, so `7` and `'7'` are the same scope everywhere. */
1600
+ type RagScopeValue = string | number;
1601
+ /** Maps a scope key to the function reading it from a row. `{}` declares a global corpus. */
1602
+ type RagScopeDefinition<Row> = Record<string, (row: Row) => RagScopeValue>;
1603
+ /** A required scope value was missing or malformed. Thrown before any embedding is paid for. */
1604
+ declare class RagScopeRequiredError extends Error {
1605
+ readonly model: string;
1606
+ readonly key: string;
1607
+ constructor(model: string, key: string);
1608
+ }
1609
+ declare const ANY_OF: unique symbol;
1610
+ /** See {@link anyOf}. */
1611
+ interface RagAnyOf<Value> {
1612
+ readonly [ANY_OF]: readonly Value[];
1613
+ }
1614
+ /**
1615
+ * Search several scope values at once. An empty list matches nothing. It is a wrapper rather than a
1616
+ * plain array so that an array arriving from a request can't widen a search by accident.
1617
+ */
1618
+ declare function anyOf<Value extends RagScopeValue>(values: readonly Value[]): RagAnyOf<Value>;
1619
+ /** The scope a search must carry: every key of the definition, with the type its function returns. */
1620
+ type RagScopeOf<Scope> = {
1621
+ [Key in keyof Scope]: Scope[Key] extends (row: never) => infer Value ? Value | RagAnyOf<Value> : never;
1622
+ };
1623
+ /** A `scope` is required unless the definition declared none. A `filter` can't name a scope key. */
1624
+ type RagSearchOptions<Scope> = {
1625
+ topK?: number;
1626
+ filter?: Record<string, unknown> & {
1627
+ [Key in keyof Scope]?: never;
1628
+ };
1629
+ } & (keyof Scope extends never ? {
1630
+ scope?: undefined;
1631
+ } : {
1632
+ scope: RagScopeOf<Scope>;
1633
+ });
1634
+ interface RagIndexDefinition<Row, Scope extends RagScopeDefinition<Row>> {
1635
+ /** Namespaces document ids (`<name>:<id>`) and the {@link RAG_ENTITY_KEY} metadata. */
1636
+ name: string;
1637
+ id: (row: Row) => RagScopeValue;
1638
+ /** The text to index. Blank removes the row's chunks. */
1639
+ text: (row: Row) => string | null | undefined;
1640
+ /** Required on purpose: a global corpus has to say `scope: {}`. */
1641
+ scope: Scope;
1642
+ /** Citation-facing origin of each chunk. */
1643
+ source?: (row: Row) => string | null | undefined;
1644
+ /** Extra chunk metadata. Can't override the scope. */
1645
+ metadata?: (row: Row) => Record<string, unknown>;
1646
+ chunk?: ChunkOptions;
1647
+ /** Keep only the first N chunks: an upper bound on what one huge document can cost. */
1648
+ maxChunks?: number;
1649
+ minScore?: EmbeddingRetrieverOptions['minScore'];
1650
+ }
1651
+ interface RagIndexDeps {
1652
+ store: VectorStore;
1653
+ embedder: EmbeddingProvider;
1654
+ /**
1655
+ * Replaces the default dense retriever (hybrid, reranked). It must honour `options.filter`, which
1656
+ * carries the scope. Embedding spend it does on its own doesn't reach `onUsage`.
1657
+ */
1658
+ retriever?: Retriever;
1659
+ /** Embedding spend, indexing and searching alike. This package can't know who pays for it. */
1660
+ onUsage?: (usage: EmbeddingUsage) => void | Promise<void>;
1661
+ }
1662
+ interface RagReconcileOptions<Scope> {
1663
+ /** Compare only this part of the corpus. `liveIds` then only has to cover that part. */
1664
+ scope?: Partial<{
1665
+ [Key in keyof Scope]: Scope[Key] extends (row: never) => infer V ? V : never;
1666
+ }>;
1667
+ /** Report without removing anything. */
1668
+ dryRun?: boolean;
1669
+ }
1670
+ interface RagReconcileResult {
1671
+ /** Indexed document ids with no live row. Removed, unless `dryRun`. */
1672
+ orphans: string[];
1673
+ /** Live document ids with no chunks: never indexed, or blank. Nothing is embedded for them here. */
1674
+ missing: string[];
1675
+ }
1676
+ interface RagToolOptions<Scope> {
1677
+ name: string;
1678
+ description?: string;
1679
+ topK?: number;
1680
+ /** From the tool context, never from the model's input. */
1681
+ scope: (ctx: AiToolCtx) => RagScopeOf<Scope>;
1682
+ }
1683
+ /**
1684
+ * The framework-free core behind `RagRepository`: one model's chunks in a shared store, with a scope
1685
+ * that a search can't omit or override. It only composes what this package already has; cost, quota
1686
+ * and access control stay in the app.
1687
+ *
1688
+ * Saving a row doesn't index it, because embedding is a slow, paid call. Call {@link RagIndex.index}
1689
+ * from a job, or wherever the text is known to have changed.
1690
+ */
1691
+ declare class RagIndex<Row, Scope extends RagScopeDefinition<Row>> {
1692
+ private readonly definition;
1693
+ private readonly deps;
1694
+ private readonly embedder;
1695
+ constructor(definition: RagIndexDefinition<Row, Scope>, deps: RagIndexDeps);
1696
+ /** The id this row's chunks are stored under: `<name>:<id>`. */
1697
+ documentId(id: RagScopeValue): string;
1698
+ /** A `Retriever` locked to this scope, for `HybridRetriever`, `createRetrievalTool` and so on. */
1699
+ scoped(scope: RagScopeOf<Scope>): Retriever;
1700
+ search(query: string, options: RagSearchOptions<Scope>): Promise<Passage[]>;
1701
+ /**
1702
+ * Replaces the row's chunks and returns how many were written. Embeddings come first: if the
1703
+ * provider fails, the previous chunks are still there. The swap is one transaction on Postgres; on a
1704
+ * store without `replaceDocument` a crash mid-swap loses the document until the next `index`.
1705
+ */
1706
+ index(row: Row): Promise<number>;
1707
+ /** Takes the id itself once the row is gone. */
1708
+ remove(rowOrId: Row | RagScopeValue): Promise<void>;
1709
+ /**
1710
+ * Diffs the index against the ids that exist, and removes what no hook could: `ON DELETE CASCADE`,
1711
+ * raw SQL, a crash after a commit. `liveIds` must be complete for the part being compared, because
1712
+ * anything left out is removed. Use `dryRun` to look first.
1713
+ */
1714
+ reconcile(liveIds: Iterable<RagScopeValue> | AsyncIterable<RagScopeValue>, options?: RagReconcileOptions<Scope>): Promise<RagReconcileResult>;
1715
+ /** A `search_*` tool whose scope comes from the context. Pass it to `provideAgentTool`. */
1716
+ tool(options: RagToolOptions<Scope>): RetrievalTool;
1717
+ private baseRetriever;
1718
+ /** The filter a scope compiles to. Throws on anything but a usable value, before any paid call. */
1719
+ private scopeFilter;
1720
+ }
1721
+ /**
1722
+ * The fields a `RedisVectorStore` must declare in `filterableFields` to serve these scopes:
1723
+ * RediSearch can only filter on a key declared up front.
1724
+ */
1725
+ declare function ragFilterableFields(...scopeKeys: string[]): string[];
1726
+
1546
1727
  /**
1547
1728
  * The data model of tree navigation retrieval: a hierarchical table of contents per long document
1548
1729
  * (PageIndex-style: sections with page ranges and summaries), built once at ingestion and read by an
@@ -2327,4 +2508,4 @@ interface NavigateDocumentTool {
2327
2508
  */
2328
2509
  declare function createNavigateDocumentTool(navigator: TreeNavigationRetriever, options?: NavigateDocumentToolOptions): NavigateDocumentTool;
2329
2510
 
2330
- export { type BuildDocumentTreeOptions, type BuiltDocumentTree, type ChunkOptions, type ChunkRecord, DEFAULT_STOP_WORDS, type DescribesRetrieval, type DetectedHeading, type DocumentNavigation, type DocumentTree, type DocumentTreeHeader, type DocumentTreeInput, type DocumentTreeNode, type DocumentTreeStore, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexDocumentTreeOptions, type IndexDocumentTreeResult, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryDocumentTreeStore, MemoryVectorStore, type MetadataPatch, type NavigateDocumentInput, type NavigateDocumentOutput, type NavigateDocumentTool, type NavigateDocumentToolOptions, type NavigateOptions, type NavigatedNode, type NavigationResult, type NavigationStep, type NodePgQueryable, type OpenAiChatTreeLlmOptions, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, PgDocumentTreeStore, type PgDocumentTreeStoreOptions, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, 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, type TreeBudget, TreeBudgetExceededError, type TreeBuildStats, type TreeHeading, type TreeLlm, type TreeLlmCache, type TreeLlmCandidate, type TreeLlmRequest, type TreeLlmResponse, type TreeLlmTask, TreeNavigationRetriever, type TreeNavigationRetrieverOptions, type TreeSection, type TreeStructureSource, type TreeSummarySource, type TreeUnit, TwoStageRetriever, type TwoStageRetrieverOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, anyTermTsquery, applyMetadataPatch, buildDocumentTree, cachedTreeLlm, chunkDocuments, chunkIndexOf, chunkText, createNavigateDocumentTool, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, detectHeadings, documentIdOf, emitRagRetrieval, estimateTokens, hasSearchSyntax, indexDocumentTree, indexTree, ingestChunks, ingestDocuments, instrumentRetriever, isBatchTooLarge, isLexicalVectorStore, keywordTerms, keywordTreeLlm, openAiChatTreeLlm, openAiEmbeddings, parseJsonReply, spanText, stableHash, toPgClient, treeLlmFromModelProvider, walkTree };
2511
+ export { type AtomicReplaceVectorStore, type BuildDocumentTreeOptions, type BuiltDocumentTree, type ChunkOptions, type ChunkRecord, DEFAULT_STOP_WORDS, type DescribesRetrieval, type DetectedHeading, type DocumentNavigation, type DocumentTree, type DocumentTreeHeader, type DocumentTreeInput, type DocumentTreeNode, type DocumentTreeStore, type DrizzleLike, type EmbeddingResult, EmbeddingRetriever, type EmbeddingRetrieverOptions, type EmbeddingUsage, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexDocumentTreeOptions, type IndexDocumentTreeResult, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryDocumentTreeStore, MemoryVectorStore, type MetadataPatch, type NavigateDocumentInput, type NavigateDocumentOutput, type NavigateDocumentTool, type NavigateDocumentToolOptions, type NavigateOptions, type NavigatedNode, type NavigationResult, type NavigationStep, type NodePgQueryable, type OpenAiChatTreeLlmOptions, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, PgDocumentTreeStore, type PgDocumentTreeStoreOptions, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, RAG_DIAGNOSTIC_LIB, RAG_ENTITY_KEY, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagAnyOf, RagIndex, type RagIndexDefinition, type RagIndexDeps, type RagReconcileOptions, type RagReconcileResult, type RagRetrievalEvent, type RagScopeDefinition, type RagScopeOf, RagScopeRequiredError, type RagScopeValue, type RagSearchOptions, type RagToolOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, type TreeBudget, TreeBudgetExceededError, type TreeBuildStats, type TreeHeading, type TreeLlm, type TreeLlmCache, type TreeLlmCandidate, type TreeLlmRequest, type TreeLlmResponse, type TreeLlmTask, TreeNavigationRetriever, type TreeNavigationRetrieverOptions, type TreeSection, type TreeStructureSource, type TreeSummarySource, type TreeUnit, TwoStageRetriever, type TwoStageRetrieverOptions, UnsafeRemovalError, type UsageReportingEmbeddingProvider, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, anyOf, anyTermTsquery, applyMetadataPatch, buildDocumentTree, cachedTreeLlm, chunkDocuments, chunkIndexOf, chunkText, createNavigateDocumentTool, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, detectHeadings, documentIdOf, embedCountingUsage, emitRagRetrieval, estimateTokens, hasSearchSyntax, indexDocumentTree, indexTree, ingestChunks, ingestDocuments, instrumentRetriever, isBatchTooLarge, isLexicalVectorStore, keywordTerms, keywordTreeLlm, openAiChatTreeLlm, openAiEmbeddings, parseJsonReply, ragFilterableFields, replaceDocumentChunks, spanText, stableHash, toPgClient, treeLlmFromModelProvider, walkTree };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Passage, Retriever, EmbeddingProvider, RetrieveOptions, Reranker, ToolSpec, ToolHandler, RerankOptions, ModelProvider, AiToolCtx } from '@dudousxd/nestjs-agent-core';
1
+ import { Passage, Retriever, EmbeddingProvider, RetrieveOptions, Reranker, ToolSpec, ToolHandler, AiToolCtx, RerankOptions, ModelProvider } from '@dudousxd/nestjs-agent-core';
2
2
 
3
3
  interface ChunkOptions {
4
4
  /** Target max characters per chunk. Default 800. */
@@ -89,7 +89,10 @@ interface VectorRecord {
89
89
  }
90
90
  interface VectorSearchOptions {
91
91
  topK: number;
92
- /** Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`. */
92
+ /**
93
+ * Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`.
94
+ * An empty array or an `undefined` value matches nothing.
95
+ */
93
96
  filter?: Record<string, unknown>;
94
97
  }
95
98
  /**
@@ -235,6 +238,8 @@ interface VectorStore {
235
238
  * got built wrong (a dropped key, an `undefined` scope) than a deliberate request to delete
236
239
  * everything. Deliberate mass deletion stays available and stays explicit:
237
240
  * `removeMany(await store.listDocumentIds())`, or the backend's own drop.
241
+ * - **A key with an `undefined` value throws** {@link UnsafeRemovalError} too, for the same reason:
242
+ * `removeWhere({ tenantId: actor.tenantRef })` with no tenant is a bug, not a request.
238
243
  *
239
244
  * What it removes is exactly what a {@link VectorStore.search} carrying the same filter could
240
245
  * reach — no more (it cannot escape the filter) and no less.
@@ -273,6 +278,16 @@ interface LexicalVectorStore extends VectorStore {
273
278
  */
274
279
  searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
275
280
  }
281
+ /**
282
+ * An **optional** capability: swap all of a document's chunks atomically, so no search or crash ever
283
+ * sees it half-replaced. Optional because atomicity is exactly what not every backend can promise over
284
+ * its client; {@link replaceDocumentChunks} falls back to `remove` plus `upsert` for the others.
285
+ */
286
+ interface AtomicReplaceVectorStore extends VectorStore {
287
+ replaceDocument(documentId: string, records: VectorRecord[]): Promise<void>;
288
+ }
289
+ /** Replaces a document's chunks, atomically where the store can. */
290
+ declare function replaceDocumentChunks(store: VectorStore, documentId: string, records: VectorRecord[]): Promise<void>;
276
291
  /**
277
292
  * Does this store carry the optional {@link LexicalVectorStore} capability? Lets a consumer pick the
278
293
  * store-backed lexical leg when available and fall back to `KeywordRetriever` (or to dense-only)
@@ -287,10 +302,10 @@ declare function isLexicalVectorStore(store: VectorStore): store is LexicalVecto
287
302
  */
288
303
  declare class UnsafeRemovalError extends Error {
289
304
  /** Why the removal was refused. */
290
- readonly reason: 'empty-filter' | 'unindexed-field';
305
+ readonly reason: 'empty-filter' | 'unindexed-field' | 'undefined-value';
291
306
  constructor(
292
307
  /** Why the removal was refused. */
293
- reason: 'empty-filter' | 'unindexed-field', message: string);
308
+ reason: 'empty-filter' | 'unindexed-field' | 'undefined-value', message: string);
294
309
  }
295
310
  /** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */
296
311
  interface IndexedDocument {
@@ -562,6 +577,13 @@ interface PgVectorStoreOptions {
562
577
  * the chunk metadata) when two models share a width.
563
578
  */
564
579
  dimensions?: number | readonly number[];
580
+ /**
581
+ * The pgvector column type. `halfvec` stores each dimension in 2 bytes instead of 4, and its HNSW
582
+ * index accepts up to 4000 dimensions where `vector` stops at 2000 (so `text-embedding-3-large`'s
583
+ * 3072 needs it). Needs a single width, not a list. Only affects the DDL for a new table and the
584
+ * casts in statements: converting an existing table is a migration of yours. Default `vector`.
585
+ */
586
+ vectorType?: 'vector' | 'halfvec';
565
587
  /**
566
588
  * Allow chunks with no embedding (`embedding: []` on upsert is stored as `NULL`): index text first
567
589
  * and embed later, or keep full-text-only chunks when there is no embedding model at all. The
@@ -617,13 +639,14 @@ interface PgVectorStoreOptions {
617
639
  * Subclassing: every statement filters through {@link PgVectorStore.whereConditions}, so a subclass
618
640
  * can map an opaque filter onto its own columns (a tenant id, an ACL) in one place.
619
641
  */
620
- declare class PgVectorStore implements VectorStore {
642
+ declare class PgVectorStore implements AtomicReplaceVectorStore {
621
643
  protected readonly client: PgClient;
622
644
  protected readonly table: string;
623
645
  /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */
624
646
  private readonly dimensions;
625
647
  /** The widths with a partial HNSW index, for a mixed-dimension table. */
626
648
  private readonly indexedDimensions;
649
+ private readonly vectorType;
627
650
  private readonly nullableEmbeddings;
628
651
  private readonly iterativeScan;
629
652
  private readonly efSearch;
@@ -645,6 +668,12 @@ declare class PgVectorStore implements VectorStore {
645
668
  * {@link PgVectorStoreOptions.nullableEmbeddings}).
646
669
  */
647
670
  upsert(records: VectorRecord[]): Promise<void>;
671
+ /**
672
+ * Swaps a document's chunks in one transaction, so a crash can't lose it and a search sees the old
673
+ * version or the new one. A client without `transaction` falls back to two statements.
674
+ */
675
+ replaceDocument(documentId: string, records: VectorRecord[]): Promise<void>;
676
+ private upsertWith;
648
677
  remove(documentId: string): Promise<void>;
649
678
  /**
650
679
  * Rewrite a document's metadata without re-embedding it. See {@link VectorStore.updateMetadata}
@@ -1394,6 +1423,11 @@ interface RetrievalToolOptions {
1394
1423
  description?: string;
1395
1424
  /** Passages per search. Default lets the retriever decide (typically 5). */
1396
1425
  topK?: number;
1426
+ /**
1427
+ * The access filter for this call, from the tool context — e.g.
1428
+ * `(ctx) => ({ tenant: ctx.actor.tenantRef })`. Without it, every actor searches the same corpus.
1429
+ */
1430
+ filter?: (ctx: AiToolCtx) => Record<string, unknown> | undefined;
1397
1431
  /**
1398
1432
  * Retrieval telemetry (`aviary:rag:retrieval` — duration, chunk count, score distribution, store
1399
1433
  * and collection). **On by default**, because this tool is the once-per-retrieval seam in agentic
@@ -1427,6 +1461,23 @@ interface RetrievalTool {
1427
1461
  */
1428
1462
  declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
1429
1463
 
1464
+ /** What one embedding call consumed: the whole batch, not per text. */
1465
+ interface EmbeddingUsage {
1466
+ inputTokens: number;
1467
+ /** The model the provider reported, needed to estimate a cost. */
1468
+ modelId?: string;
1469
+ }
1470
+ interface EmbeddingResult {
1471
+ vectors: number[][];
1472
+ usage?: EmbeddingUsage;
1473
+ }
1474
+ /** An {@link EmbeddingProvider} that can also say what a call cost. `openAiEmbeddings` is one. */
1475
+ interface UsageReportingEmbeddingProvider extends EmbeddingProvider {
1476
+ embedWithUsage(texts: string[]): Promise<EmbeddingResult>;
1477
+ }
1478
+ /** Calls `embedWithUsage` when the provider has it, otherwise `embed` with no usage. */
1479
+ declare function embedCountingUsage(provider: EmbeddingProvider, texts: string[]): Promise<EmbeddingResult>;
1480
+
1430
1481
  /** A non-2xx answer (or a malformed one) from an embeddings or rerank endpoint. */
1431
1482
  declare class HttpModelError extends Error {
1432
1483
  /** HTTP status; `502` when the endpoint answered 2xx with a body that doesn't fit the contract. */
@@ -1497,7 +1548,7 @@ declare function isBatchTooLarge(error: unknown): boolean;
1497
1548
  * const local = openAiEmbeddings({ baseUrl: 'http://tei:8080/v1', model: 'BAAI/bge-m3' });
1498
1549
  * ```
1499
1550
  */
1500
- declare function openAiEmbeddings(options: OpenAiEmbeddingsOptions): EmbeddingProvider;
1551
+ declare function openAiEmbeddings(options: OpenAiEmbeddingsOptions): UsageReportingEmbeddingProvider;
1501
1552
 
1502
1553
  interface HttpRerankerOptions {
1503
1554
  /**
@@ -1543,6 +1594,136 @@ declare class HttpReranker implements Reranker {
1543
1594
  rerank(query: string, passages: Passage[], options?: RerankOptions): Promise<Passage[]>;
1544
1595
  }
1545
1596
 
1597
+ /** The metadata key separating models that share one store. Reserved: it always wins over yours. */
1598
+ declare const RAG_ENTITY_KEY = "ragEntity";
1599
+ /** What a scope value can be. Compared as strings, so `7` and `'7'` are the same scope everywhere. */
1600
+ type RagScopeValue = string | number;
1601
+ /** Maps a scope key to the function reading it from a row. `{}` declares a global corpus. */
1602
+ type RagScopeDefinition<Row> = Record<string, (row: Row) => RagScopeValue>;
1603
+ /** A required scope value was missing or malformed. Thrown before any embedding is paid for. */
1604
+ declare class RagScopeRequiredError extends Error {
1605
+ readonly model: string;
1606
+ readonly key: string;
1607
+ constructor(model: string, key: string);
1608
+ }
1609
+ declare const ANY_OF: unique symbol;
1610
+ /** See {@link anyOf}. */
1611
+ interface RagAnyOf<Value> {
1612
+ readonly [ANY_OF]: readonly Value[];
1613
+ }
1614
+ /**
1615
+ * Search several scope values at once. An empty list matches nothing. It is a wrapper rather than a
1616
+ * plain array so that an array arriving from a request can't widen a search by accident.
1617
+ */
1618
+ declare function anyOf<Value extends RagScopeValue>(values: readonly Value[]): RagAnyOf<Value>;
1619
+ /** The scope a search must carry: every key of the definition, with the type its function returns. */
1620
+ type RagScopeOf<Scope> = {
1621
+ [Key in keyof Scope]: Scope[Key] extends (row: never) => infer Value ? Value | RagAnyOf<Value> : never;
1622
+ };
1623
+ /** A `scope` is required unless the definition declared none. A `filter` can't name a scope key. */
1624
+ type RagSearchOptions<Scope> = {
1625
+ topK?: number;
1626
+ filter?: Record<string, unknown> & {
1627
+ [Key in keyof Scope]?: never;
1628
+ };
1629
+ } & (keyof Scope extends never ? {
1630
+ scope?: undefined;
1631
+ } : {
1632
+ scope: RagScopeOf<Scope>;
1633
+ });
1634
+ interface RagIndexDefinition<Row, Scope extends RagScopeDefinition<Row>> {
1635
+ /** Namespaces document ids (`<name>:<id>`) and the {@link RAG_ENTITY_KEY} metadata. */
1636
+ name: string;
1637
+ id: (row: Row) => RagScopeValue;
1638
+ /** The text to index. Blank removes the row's chunks. */
1639
+ text: (row: Row) => string | null | undefined;
1640
+ /** Required on purpose: a global corpus has to say `scope: {}`. */
1641
+ scope: Scope;
1642
+ /** Citation-facing origin of each chunk. */
1643
+ source?: (row: Row) => string | null | undefined;
1644
+ /** Extra chunk metadata. Can't override the scope. */
1645
+ metadata?: (row: Row) => Record<string, unknown>;
1646
+ chunk?: ChunkOptions;
1647
+ /** Keep only the first N chunks: an upper bound on what one huge document can cost. */
1648
+ maxChunks?: number;
1649
+ minScore?: EmbeddingRetrieverOptions['minScore'];
1650
+ }
1651
+ interface RagIndexDeps {
1652
+ store: VectorStore;
1653
+ embedder: EmbeddingProvider;
1654
+ /**
1655
+ * Replaces the default dense retriever (hybrid, reranked). It must honour `options.filter`, which
1656
+ * carries the scope. Embedding spend it does on its own doesn't reach `onUsage`.
1657
+ */
1658
+ retriever?: Retriever;
1659
+ /** Embedding spend, indexing and searching alike. This package can't know who pays for it. */
1660
+ onUsage?: (usage: EmbeddingUsage) => void | Promise<void>;
1661
+ }
1662
+ interface RagReconcileOptions<Scope> {
1663
+ /** Compare only this part of the corpus. `liveIds` then only has to cover that part. */
1664
+ scope?: Partial<{
1665
+ [Key in keyof Scope]: Scope[Key] extends (row: never) => infer V ? V : never;
1666
+ }>;
1667
+ /** Report without removing anything. */
1668
+ dryRun?: boolean;
1669
+ }
1670
+ interface RagReconcileResult {
1671
+ /** Indexed document ids with no live row. Removed, unless `dryRun`. */
1672
+ orphans: string[];
1673
+ /** Live document ids with no chunks: never indexed, or blank. Nothing is embedded for them here. */
1674
+ missing: string[];
1675
+ }
1676
+ interface RagToolOptions<Scope> {
1677
+ name: string;
1678
+ description?: string;
1679
+ topK?: number;
1680
+ /** From the tool context, never from the model's input. */
1681
+ scope: (ctx: AiToolCtx) => RagScopeOf<Scope>;
1682
+ }
1683
+ /**
1684
+ * The framework-free core behind `RagRepository`: one model's chunks in a shared store, with a scope
1685
+ * that a search can't omit or override. It only composes what this package already has; cost, quota
1686
+ * and access control stay in the app.
1687
+ *
1688
+ * Saving a row doesn't index it, because embedding is a slow, paid call. Call {@link RagIndex.index}
1689
+ * from a job, or wherever the text is known to have changed.
1690
+ */
1691
+ declare class RagIndex<Row, Scope extends RagScopeDefinition<Row>> {
1692
+ private readonly definition;
1693
+ private readonly deps;
1694
+ private readonly embedder;
1695
+ constructor(definition: RagIndexDefinition<Row, Scope>, deps: RagIndexDeps);
1696
+ /** The id this row's chunks are stored under: `<name>:<id>`. */
1697
+ documentId(id: RagScopeValue): string;
1698
+ /** A `Retriever` locked to this scope, for `HybridRetriever`, `createRetrievalTool` and so on. */
1699
+ scoped(scope: RagScopeOf<Scope>): Retriever;
1700
+ search(query: string, options: RagSearchOptions<Scope>): Promise<Passage[]>;
1701
+ /**
1702
+ * Replaces the row's chunks and returns how many were written. Embeddings come first: if the
1703
+ * provider fails, the previous chunks are still there. The swap is one transaction on Postgres; on a
1704
+ * store without `replaceDocument` a crash mid-swap loses the document until the next `index`.
1705
+ */
1706
+ index(row: Row): Promise<number>;
1707
+ /** Takes the id itself once the row is gone. */
1708
+ remove(rowOrId: Row | RagScopeValue): Promise<void>;
1709
+ /**
1710
+ * Diffs the index against the ids that exist, and removes what no hook could: `ON DELETE CASCADE`,
1711
+ * raw SQL, a crash after a commit. `liveIds` must be complete for the part being compared, because
1712
+ * anything left out is removed. Use `dryRun` to look first.
1713
+ */
1714
+ reconcile(liveIds: Iterable<RagScopeValue> | AsyncIterable<RagScopeValue>, options?: RagReconcileOptions<Scope>): Promise<RagReconcileResult>;
1715
+ /** A `search_*` tool whose scope comes from the context. Pass it to `provideAgentTool`. */
1716
+ tool(options: RagToolOptions<Scope>): RetrievalTool;
1717
+ private baseRetriever;
1718
+ /** The filter a scope compiles to. Throws on anything but a usable value, before any paid call. */
1719
+ private scopeFilter;
1720
+ }
1721
+ /**
1722
+ * The fields a `RedisVectorStore` must declare in `filterableFields` to serve these scopes:
1723
+ * RediSearch can only filter on a key declared up front.
1724
+ */
1725
+ declare function ragFilterableFields(...scopeKeys: string[]): string[];
1726
+
1546
1727
  /**
1547
1728
  * The data model of tree navigation retrieval: a hierarchical table of contents per long document
1548
1729
  * (PageIndex-style: sections with page ranges and summaries), built once at ingestion and read by an
@@ -2327,4 +2508,4 @@ interface NavigateDocumentTool {
2327
2508
  */
2328
2509
  declare function createNavigateDocumentTool(navigator: TreeNavigationRetriever, options?: NavigateDocumentToolOptions): NavigateDocumentTool;
2329
2510
 
2330
- export { type BuildDocumentTreeOptions, type BuiltDocumentTree, type ChunkOptions, type ChunkRecord, DEFAULT_STOP_WORDS, type DescribesRetrieval, type DetectedHeading, type DocumentNavigation, type DocumentTree, type DocumentTreeHeader, type DocumentTreeInput, type DocumentTreeNode, type DocumentTreeStore, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexDocumentTreeOptions, type IndexDocumentTreeResult, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryDocumentTreeStore, MemoryVectorStore, type MetadataPatch, type NavigateDocumentInput, type NavigateDocumentOutput, type NavigateDocumentTool, type NavigateDocumentToolOptions, type NavigateOptions, type NavigatedNode, type NavigationResult, type NavigationStep, type NodePgQueryable, type OpenAiChatTreeLlmOptions, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, PgDocumentTreeStore, type PgDocumentTreeStoreOptions, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, 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, type TreeBudget, TreeBudgetExceededError, type TreeBuildStats, type TreeHeading, type TreeLlm, type TreeLlmCache, type TreeLlmCandidate, type TreeLlmRequest, type TreeLlmResponse, type TreeLlmTask, TreeNavigationRetriever, type TreeNavigationRetrieverOptions, type TreeSection, type TreeStructureSource, type TreeSummarySource, type TreeUnit, TwoStageRetriever, type TwoStageRetrieverOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, anyTermTsquery, applyMetadataPatch, buildDocumentTree, cachedTreeLlm, chunkDocuments, chunkIndexOf, chunkText, createNavigateDocumentTool, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, detectHeadings, documentIdOf, emitRagRetrieval, estimateTokens, hasSearchSyntax, indexDocumentTree, indexTree, ingestChunks, ingestDocuments, instrumentRetriever, isBatchTooLarge, isLexicalVectorStore, keywordTerms, keywordTreeLlm, openAiChatTreeLlm, openAiEmbeddings, parseJsonReply, spanText, stableHash, toPgClient, treeLlmFromModelProvider, walkTree };
2511
+ export { type AtomicReplaceVectorStore, type BuildDocumentTreeOptions, type BuiltDocumentTree, type ChunkOptions, type ChunkRecord, DEFAULT_STOP_WORDS, type DescribesRetrieval, type DetectedHeading, type DocumentNavigation, type DocumentTree, type DocumentTreeHeader, type DocumentTreeInput, type DocumentTreeNode, type DocumentTreeStore, type DrizzleLike, type EmbeddingResult, EmbeddingRetriever, type EmbeddingRetrieverOptions, type EmbeddingUsage, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexDocumentTreeOptions, type IndexDocumentTreeResult, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryDocumentTreeStore, MemoryVectorStore, type MetadataPatch, type NavigateDocumentInput, type NavigateDocumentOutput, type NavigateDocumentTool, type NavigateDocumentToolOptions, type NavigateOptions, type NavigatedNode, type NavigationResult, type NavigationStep, type NodePgQueryable, type OpenAiChatTreeLlmOptions, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, PgDocumentTreeStore, type PgDocumentTreeStoreOptions, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, RAG_DIAGNOSTIC_LIB, RAG_ENTITY_KEY, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagAnyOf, RagIndex, type RagIndexDefinition, type RagIndexDeps, type RagReconcileOptions, type RagReconcileResult, type RagRetrievalEvent, type RagScopeDefinition, type RagScopeOf, RagScopeRequiredError, type RagScopeValue, type RagSearchOptions, type RagToolOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, type TreeBudget, TreeBudgetExceededError, type TreeBuildStats, type TreeHeading, type TreeLlm, type TreeLlmCache, type TreeLlmCandidate, type TreeLlmRequest, type TreeLlmResponse, type TreeLlmTask, TreeNavigationRetriever, type TreeNavigationRetrieverOptions, type TreeSection, type TreeStructureSource, type TreeSummarySource, type TreeUnit, TwoStageRetriever, type TwoStageRetrieverOptions, UnsafeRemovalError, type UsageReportingEmbeddingProvider, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, anyOf, anyTermTsquery, applyMetadataPatch, buildDocumentTree, cachedTreeLlm, chunkDocuments, chunkIndexOf, chunkText, createNavigateDocumentTool, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, detectHeadings, documentIdOf, embedCountingUsage, emitRagRetrieval, estimateTokens, hasSearchSyntax, indexDocumentTree, indexTree, ingestChunks, ingestDocuments, instrumentRetriever, isBatchTooLarge, isLexicalVectorStore, keywordTerms, keywordTreeLlm, openAiChatTreeLlm, openAiEmbeddings, parseJsonReply, ragFilterableFields, replaceDocumentChunks, spanText, stableHash, toPgClient, treeLlmFromModelProvider, walkTree };