@dudousxd/nestjs-agent-rag 0.4.0 → 0.5.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 +122 -3
- package/dist/index.cjs +250 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +177 -4
- package/dist/index.d.ts +177 -4
- package/dist/index.js +246 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/chunk.ts","../src/vector-store.ts","../src/filter.ts","../src/memory-vector-store.ts","../src/pg-vector-store.ts","../src/redis-vector-store.ts","../src/embedding-retriever.ts","../src/keyword-retriever.ts","../src/hybrid-retriever.ts","../src/reranking-retriever.ts","../src/filtered-retriever.ts","../src/ingest.ts","../src/retrieval-tool.ts"],"sourcesContent":["export { chunkText, type ChunkOptions } from './chunk.js';\nexport type {\n IndexedDocument,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\nexport { documentIdOf } from './vector-store.js';\nexport { MemoryVectorStore } from './memory-vector-store.js';\nexport {\n PgVectorStore,\n type PgClient,\n type PgVectorStoreOptions,\n} from './pg-vector-store.js';\nexport {\n RedisVectorStore,\n type RedisSearchClient,\n type RedisVectorStoreOptions,\n} from './redis-vector-store.js';\nexport { EmbeddingRetriever } from './embedding-retriever.js';\nexport { KeywordRetriever, type KeywordRetrieverOptions } from './keyword-retriever.js';\nexport { HybridRetriever, type HybridRetrieverOptions } from './hybrid-retriever.js';\nexport { RerankingRetriever, type RerankingRetrieverOptions } from './reranking-retriever.js';\nexport { FilteredRetriever } from './filtered-retriever.js';\nexport {\n chunkDocuments,\n ingestChunks,\n ingestDocuments,\n type ChunkRecord,\n type IngestChunksOptions,\n type IngestDocument,\n type IngestOptions,\n} from './ingest.js';\nexport {\n createRetrievalTool,\n type RetrievalTool,\n type RetrievalToolOptions,\n} from './retrieval-tool.js';\n","export interface ChunkOptions {\n /** Target max characters per chunk. Default 800. */\n chunkSize?: number;\n /** Characters of overlap carried from the end of one chunk into the next. Default 100. */\n overlap?: number;\n}\n\n/**\n * Splits text into overlapping chunks for embedding. Greedy up to `chunkSize`, but prefers to break\n * on a paragraph → sentence → word boundary in the back half of the window (so a chunk rarely cuts\n * mid-sentence), then carries `overlap` characters into the next chunk to preserve context across\n * the seam. Pure and deterministic.\n */\nexport function chunkText(text: string, options: ChunkOptions = {}): string[] {\n const chunkSize = options.chunkSize ?? 800;\n const overlap = options.overlap ?? 100;\n const normalized = text.trim();\n if (normalized.length === 0) {\n return [];\n }\n if (normalized.length <= chunkSize) {\n return [normalized];\n }\n\n const chunks: string[] = [];\n let start = 0;\n while (start < normalized.length) {\n const end = Math.min(start + chunkSize, normalized.length);\n let breakAt = end;\n if (end < normalized.length) {\n const window = normalized.slice(start, end);\n const boundary = Math.max(\n window.lastIndexOf('\\n\\n'),\n window.lastIndexOf('. '),\n window.lastIndexOf(' '),\n );\n // Only honour a boundary in the back half, else a tiny chunk cascades.\n if (boundary > chunkSize / 2) {\n breakAt = start + boundary + 1;\n }\n }\n const chunk = normalized.slice(start, breakAt).trim();\n if (chunk.length > 0) {\n chunks.push(chunk);\n }\n if (breakAt >= normalized.length) {\n break;\n }\n // Step forward with overlap, but always make progress (guards against a stuck boundary).\n start = Math.max(breakAt - overlap, start + 1);\n }\n return chunks;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\n\n/** A stored, embedded chunk. `embedding` length must match the store's configured dimensions. */\nexport interface VectorRecord {\n id: string;\n text: string;\n embedding: number[];\n /** Citation-facing origin (document title, URL, row id). */\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface VectorSearchOptions {\n topK: number;\n /** Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`. */\n filter?: Record<string, unknown>;\n}\n\n/**\n * The write + search side of RAG storage. `@dudousxd/nestjs-agent-rag` ships `MemoryVectorStore`\n * (in-JS cosine, tests + small scale) and `PgVectorStore` (pgvector); any impl works. Pair one with\n * an `EmbeddingProvider` via {@link import('./embedding-retriever.js').EmbeddingRetriever} to get a\n * core `Retriever`.\n */\nexport interface VectorStore {\n upsert(records: VectorRecord[]): Promise<void>;\n search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;\n /**\n * Delete every chunk belonging to a source document — all records whose id is `${documentId}`\n * or `${documentId}#<n>` (the id scheme {@link import('./ingest.js').chunkDocuments} produces).\n * Use it to drop a document from the index, and before re-ingesting one: `upsert` overwrites\n * matching ids but can't remove chunks a shorter new version no longer produces, so a re-ingest\n * without a preceding `remove` leaves the old tail orphaned.\n */\n remove(documentId: string): Promise<void>;\n /**\n * List the distinct source documents currently indexed (chunk ids collapsed back to their document\n * by stripping the trailing `#<n>`), each with a representative chunk's `metadata`, optionally\n * narrowed by a metadata `filter`. The enumeration seam for reconciliation: diff this against your\n * source of truth to find documents to ingest (missing), re-ingest (a stored fingerprint like\n * `size` changed), or {@link VectorStore.remove remove} (orphaned).\n */\n listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;\n}\n\n/** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */\nexport interface IndexedDocument {\n id: string;\n metadata?: Record<string, unknown>;\n}\n\n/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */\nexport function documentIdOf(chunkId: string): string {\n return chunkId.replace(/#\\d+$/, '');\n}\n","/** Metadata filter, shared by the in-memory {@link import('./memory-vector-store.js').MemoryVectorStore}\n * and {@link import('./keyword-retriever.js').KeywordRetriever}. Every key must match:\n *\n * - a **scalar** filter value keeps exact-match — the record's value must equal it;\n * - an **array** filter value means **match-any** (OR / set membership) — the record matches when its\n * value for that key is one of them, or (for a multi-valued record) shares at least one element.\n * An empty array matches nothing (no value is a member of the empty set) — the deny primitive.\n *\n * Array-valued filters are how a caller expresses capability-style access control over a shared store\n * (e.g. `{ audience: ['public', 'role:ADMIN', 'base:…'] }`) without the store knowing what a token means.\n */\nexport function matchesFilter(\n metadata: Record<string, unknown> | undefined,\n filter: Record<string, unknown>,\n): boolean {\n const entries = Object.entries(filter);\n if (metadata === undefined) {\n return entries.length === 0;\n }\n return entries.every(([key, expected]) => {\n if (Array.isArray(expected)) {\n const actual = metadata[key];\n return Array.isArray(actual)\n ? actual.some((value) => expected.includes(value))\n : expected.includes(actual);\n }\n return metadata[key] === expected;\n });\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { matchesFilter } from './filter.js';\nimport {\n type IndexedDocument,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter\n * for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).\n */\nexport class MemoryVectorStore implements VectorStore {\n private readonly records = new Map<string, VectorRecord>();\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n this.records.set(record.id, record);\n }\n }\n\n async remove(documentId: string): Promise<void> {\n for (const id of this.records.keys()) {\n if (documentIdOf(id) === documentId) {\n this.records.delete(id);\n }\n }\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const documents = new Map<string, IndexedDocument>();\n for (const record of this.records.values()) {\n if (filter !== undefined && !matchesFilter(record.metadata, filter)) {\n continue;\n }\n const id = documentIdOf(record.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return [...documents.values()];\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n const scored: Passage[] = [];\n for (const record of this.records.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n scored.push({\n id: record.id,\n text: record.text,\n score: cosineSimilarity(embedding, record.embedding),\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK);\n }\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n let dot = 0;\n let normA = 0;\n let normB = 0;\n const length = Math.min(a.length, b.length);\n for (let index = 0; index < length; index += 1) {\n const valueA = a[index] ?? 0;\n const valueB = b[index] ?? 0;\n dot += valueA * valueB;\n normA += valueA * valueA;\n normB += valueB * valueB;\n }\n const denominator = Math.sqrt(normA) * Math.sqrt(normB);\n return denominator === 0 ? 0 : dot / denominator;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport type {\n IndexedDocument,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\n\n/**\n * The minimal Postgres surface {@link PgVectorStore} needs — adapt your own `pg` / `postgres.js`\n * client to it, so this package pulls in NO driver (bring your own, like the store adapters take an\n * ORM handle and the Redis transport takes a client). `query` runs parameterized SQL (`$1,$2,…`) and\n * resolves the result rows.\n */\nexport interface PgClient {\n query<Row = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<Row[]>;\n}\n\nexport interface PgVectorStoreOptions {\n /** Table name. Default `agent_rag_chunks`. */\n table?: string;\n /** Embedding width — must match your model (e.g. 1536 for text-embedding-3-small). Default 1536. */\n dimensions?: number;\n}\n\n/**\n * A pgvector-backed {@link VectorStore} — the production reference adapter. Cosine distance via the\n * `<=>` operator over an HNSW index; metadata in a `jsonb` column filtered with `@>`. Call\n * {@link PgVectorStore.ensureSchema} once at boot to create the extension, table, and index.\n */\nexport class PgVectorStore implements VectorStore {\n private readonly table: string;\n private readonly dimensions: number;\n\n constructor(\n private readonly client: PgClient,\n options: PgVectorStoreOptions = {},\n ) {\n this.table = options.table ?? 'agent_rag_chunks';\n this.dimensions = options.dimensions ?? 1536;\n }\n\n /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */\n async ensureSchema(): Promise<void> {\n await this.client.query('CREATE EXTENSION IF NOT EXISTS vector');\n await this.client.query(\n `CREATE TABLE IF NOT EXISTS ${this.table} (\n id TEXT PRIMARY KEY,\n text TEXT NOT NULL,\n source TEXT,\n metadata JSONB,\n embedding vector(${this.dimensions}) NOT NULL\n )`,\n );\n await this.client.query(\n `CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx\n ON ${this.table} USING hnsw (embedding vector_cosine_ops)`,\n );\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n await this.client.query(\n `INSERT INTO ${this.table} (id, text, source, metadata, embedding)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (id) DO UPDATE SET\n text = EXCLUDED.text,\n source = EXCLUDED.source,\n metadata = EXCLUDED.metadata,\n embedding = EXCLUDED.embedding`,\n [\n record.id,\n record.text,\n record.source ?? null,\n record.metadata !== undefined ? JSON.stringify(record.metadata) : null,\n toVectorLiteral(record.embedding),\n ],\n );\n }\n }\n\n async remove(documentId: string): Promise<void> {\n await this.client.query(`DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1`, [\n documentId,\n ]);\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const where = buildWhere(filter, 1);\n // DISTINCT ON collapses chunks to one row per document; all chunks share the doc's metadata.\n const rows = await this.client.query<{\n doc_id: string;\n metadata: Record<string, unknown> | null;\n }>(\n `SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata\n FROM ${this.table}\n ${where.sql}\n ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`,\n where.params,\n );\n return rows.map((row) => ({\n id: row.doc_id,\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n const vector = toVectorLiteral(embedding);\n // $1 = query vector, $2 = topK; metadata-filter params start at $3.\n const where = buildWhere(options.filter, 3);\n const rows = await this.client.query<PgRow>(\n `SELECT id, text, source, metadata, 1 - (embedding <=> $1::vector) AS score\n FROM ${this.table}\n ${where.sql}\n ORDER BY embedding <=> $1::vector\n LIMIT $2`,\n [vector, options.topK, ...where.params],\n );\n return rows.map((row) => ({\n id: row.id,\n text: row.text,\n score: Number(row.score),\n ...(row.source !== null ? { source: row.source } : {}),\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n}\n\ninterface PgRow {\n id: string;\n text: string;\n source: string | null;\n metadata: Record<string, unknown> | null;\n score: number | string;\n}\n\n/**\n * Build the `WHERE` fragment for a metadata `filter`, with parameter placeholders numbered from\n * `startIndex`. Scalar values collapse into a single `@>` jsonb-containment check (exact-match, as\n * before). An **array** value is a **match-any** (OR / set membership) check via jsonb `?|`: the\n * record matches when its value for that key — scalar or array — shares an element with the filter\n * array. An empty array can never match (deny primitive). Keys are passed as parameters (`metadata->$k`)\n * so a caller-supplied metadata key can't inject SQL. Returns `{ sql: '', params: [] }` when there is\n * no filter, preserving the previous unfiltered query shape.\n */\nfunction buildWhere(\n filter: Record<string, unknown> | undefined,\n startIndex: number,\n): { sql: string; params: unknown[] } {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return { sql: '', params: [] };\n }\n const clauses: string[] = [];\n const params: unknown[] = [];\n const scalar: Record<string, unknown> = {};\n let index = startIndex;\n for (const [key, value] of Object.entries(filter)) {\n if (Array.isArray(value)) {\n if (value.length === 0) {\n clauses.push('false');\n continue;\n }\n const keyParam = `$${index}`;\n params.push(key);\n index += 1;\n const arrParam = `$${index}`;\n params.push(value.map(String));\n index += 1;\n clauses.push(\n `(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' ` +\n `THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`,\n );\n } else {\n scalar[key] = value;\n }\n }\n if (Object.keys(scalar).length > 0) {\n clauses.push(`metadata @> $${index}::jsonb`);\n params.push(JSON.stringify(scalar));\n index += 1;\n }\n return { sql: `WHERE ${clauses.join(' AND ')}`, params };\n}\n\n/**\n * SQL that collapses a chunk id (`${documentId}#<n>`) back to its source document id — the pgvector\n * mirror of {@link import('./vector-store.js').documentIdOf}. Shared by `remove` and\n * `listDocuments` so both key on the exact same definition of \"chunk belongs to document\".\n */\nconst DOCUMENT_ID_FROM_CHUNK = \"regexp_replace(id, '#[0-9]+$', '')\";\n\n/** pgvector accepts a `'[1,2,3]'` text literal cast to `vector`. */\nfunction toVectorLiteral(embedding: number[]): string {\n return `[${embedding.join(',')}]`;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport {\n type IndexedDocument,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * The minimal Redis surface {@link RedisVectorStore} needs: send a raw command and get the reply.\n * Both `node-redis` (`client.sendCommand(args)`) and `ioredis` (wrap `redis.call`) satisfy it — so\n * this package pulls in NO Redis driver (bring your own, like the other adapters). Requires the\n * RediSearch module (Redis Stack / Redis 8+).\n */\nexport interface RedisSearchClient {\n sendCommand(args: (string | Buffer)[]): Promise<unknown>;\n}\n\nexport interface RedisVectorStoreOptions {\n /** RediSearch index name. Default `agent_rag_idx`. */\n index?: string;\n /** Key prefix for the per-chunk hashes. Default `agent_rag:`. */\n prefix?: string;\n /** Embedding width — must match your model (e.g. 1536). Default 1536. */\n dimensions?: number;\n /**\n * Metadata keys to index as filterable TAG fields. RediSearch needs filter fields declared up\n * front, so only keys listed here can be passed in `search`'s `filter`. Default none.\n */\n filterableFields?: string[];\n}\n\n/**\n * A RediSearch-backed {@link VectorStore} — HNSW + cosine over Redis, the ecosystem-native option\n * for anyone already running Redis (see `-transport-redis`). Chunks are hashes under `prefix`, the\n * embedding a FLOAT32 vector field; `search` is a KNN query. Call {@link RedisVectorStore.ensureSchema}\n * once at boot to create the index.\n */\nexport class RedisVectorStore implements VectorStore {\n private readonly index: string;\n private readonly prefix: string;\n private readonly dimensions: number;\n private readonly filterableFields: string[];\n\n constructor(\n private readonly client: RedisSearchClient,\n options: RedisVectorStoreOptions = {},\n ) {\n this.index = options.index ?? 'agent_rag_idx';\n this.prefix = options.prefix ?? 'agent_rag:';\n this.dimensions = options.dimensions ?? 1536;\n this.filterableFields = options.filterableFields ?? [];\n }\n\n /** Idempotent: create the HNSW/cosine index if `FT.INFO` says it doesn't exist yet. */\n async ensureSchema(): Promise<void> {\n try {\n await this.client.sendCommand(['FT.INFO', this.index]);\n return;\n } catch {\n // index missing → create it below\n }\n const schema: string[] = ['text', 'TEXT', 'source', 'TAG', 'metadata_json', 'TEXT', 'NOINDEX'];\n for (const field of this.filterableFields) {\n schema.push(`meta_${field}`, 'TAG');\n }\n schema.push(\n 'embedding',\n 'VECTOR',\n 'HNSW',\n '6',\n 'TYPE',\n 'FLOAT32',\n 'DIM',\n String(this.dimensions),\n 'DISTANCE_METRIC',\n 'COSINE',\n );\n await this.client.sendCommand([\n 'FT.CREATE',\n this.index,\n 'ON',\n 'HASH',\n 'PREFIX',\n '1',\n this.prefix,\n 'SCHEMA',\n ...schema,\n ]);\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n const args: (string | Buffer)[] = [\n 'HSET',\n `${this.prefix}${record.id}`,\n 'text',\n record.text,\n 'embedding',\n encodeVector(record.embedding),\n ];\n if (record.source !== undefined) {\n args.push('source', record.source);\n }\n if (record.metadata !== undefined) {\n args.push('metadata_json', JSON.stringify(record.metadata));\n for (const field of this.filterableFields) {\n const value = record.metadata[field];\n if (value !== undefined) {\n // Array metadata is stored as a multi-valued TAG (comma-separated, RediSearch's default\n // separator) so a document can carry several capability tokens; individual values must\n // therefore not contain commas.\n const tag = Array.isArray(value) ? value.map(String).join(',') : String(value);\n args.push(`meta_${field}`, tag);\n }\n }\n }\n await this.client.sendCommand(args);\n }\n }\n\n async remove(documentId: string): Promise<void> {\n const keys = new Set<string>([`${this.prefix}${documentId}`]);\n const pattern = `${this.prefix}${escapeGlob(documentId)}#*`;\n let cursor = '0';\n do {\n const reply = await this.client.sendCommand([\n 'SCAN',\n cursor,\n 'MATCH',\n pattern,\n 'COUNT',\n '256',\n ]);\n const page = parseScanReply(reply);\n cursor = page.cursor;\n for (const key of page.keys) {\n keys.add(key);\n }\n } while (cursor !== '0');\n if (keys.size > 0) {\n await this.client.sendCommand(['DEL', ...keys]);\n }\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const query = buildFilter(filter);\n const documents = new Map<string, IndexedDocument>();\n const batchSize = 1000;\n let offset = 0;\n for (;;) {\n // RETURN metadata_json (not NOCONTENT) so we can carry a representative chunk's metadata;\n // parseSearchReply already yields prefix-stripped ids + parsed metadata for both RESP2/RESP3.\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'RETURN',\n '1',\n 'metadata_json',\n 'LIMIT',\n String(offset),\n String(batchSize),\n 'DIALECT',\n '2',\n ]);\n const passages = parseSearchReply(reply, this.prefix);\n for (const passage of passages) {\n const id = documentIdOf(passage.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(passage.metadata !== undefined ? { metadata: passage.metadata } : {}),\n });\n }\n }\n if (passages.length < batchSize) {\n break;\n }\n offset += batchSize;\n }\n return [...documents.values()];\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter)) {\n return [];\n }\n const query = `${buildFilter(options.filter)}=>[KNN ${options.topK} @embedding $BLOB AS vector_score]`;\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'PARAMS',\n '2',\n 'BLOB',\n encodeVector(embedding),\n 'RETURN',\n '4',\n 'text',\n 'source',\n 'metadata_json',\n 'vector_score',\n 'SORTBY',\n 'vector_score',\n 'ASC',\n 'DIALECT',\n '2',\n 'LIMIT',\n '0',\n String(options.topK),\n ]);\n return parseSearchReply(reply, this.prefix);\n }\n}\n\n/** RediSearch expects a little-endian FLOAT32 buffer for the query/stored vector. */\nfunction encodeVector(embedding: number[]): Buffer {\n return Buffer.from(new Float32Array(embedding).buffer);\n}\n\n/**\n * `*` for no filter, else an AND of TAG clauses. A scalar value is an exact tag match\n * (`@meta_owner:{u1}`); an **array** value is a TAG alternation — match-any / OR\n * (`@meta_audience:{public|role\\:ADMIN}`), the capability-ACL primitive. RediSearch TAG fields are\n * multi-valued per document, so this also matches documents carrying several of the tags.\n */\nfunction buildFilter(filter?: Record<string, unknown>): string {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return '*';\n }\n const clauses = Object.entries(filter).map(([key, value]) => {\n const values = Array.isArray(value) ? value : [value];\n const alternation = values.map((entry) => escapeTag(String(entry))).join('|');\n return `@meta_${key}:{${alternation}}`;\n });\n return `(${clauses.join(' ')})`;\n}\n\n/**\n * A filter with an empty-array value can never match (nothing is a member of the empty set) — and\n * RediSearch has no valid empty-tag syntax, so callers short-circuit to an empty result instead of\n * emitting a broken query. This is the deny primitive (e.g. an actor with no capability tokens).\n */\nfunction filterMatchesNothing(filter?: Record<string, unknown>): boolean {\n if (filter === undefined) {\n return false;\n }\n return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);\n}\n\n/** Escape RediSearch TAG punctuation so an id/tenant value matches literally. */\nfunction escapeTag(value: string): string {\n return value.replace(/[^a-zA-Z0-9_]/g, (char) => `\\\\${char}`);\n}\n\n/** Escape `SCAN MATCH` glob metacharacters so a document id matches its chunk keys literally. */\nfunction escapeGlob(value: string): string {\n return value.replace(/[*?[\\]\\\\]/g, (char) => `\\\\${char}`);\n}\n\n/**\n * Parse a `SCAN` reply into `{ cursor, keys }`, tolerating both wire shapes: the RESP2 array\n * `[cursor, [key, …]]` and node-redis's RESP3 object `{ cursor, keys }`.\n */\nfunction parseScanReply(reply: unknown): { cursor: string; keys: string[] } {\n if (Array.isArray(reply) && reply.length >= 2) {\n const rawKeys = reply[1];\n return {\n cursor: toStr(reply[0]),\n keys: Array.isArray(rawKeys) ? rawKeys.map(toStr) : [],\n };\n }\n if (typeof reply === 'object' && reply !== null && 'cursor' in reply && 'keys' in reply) {\n const { cursor, keys } = reply as { cursor: unknown; keys: unknown };\n return {\n cursor: toStr(cursor),\n keys: Array.isArray(keys) ? keys.map(toStr) : [],\n };\n }\n return { cursor: '0', keys: [] };\n}\n\nfunction toStr(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);\n}\n\n/**\n * Parse an `FT.SEARCH` reply into passages, tolerating both wire shapes: the RESP2 array\n * `[total, key, [f, v, …], …]` (ioredis / node-redis RESP2) and the RESP3 object\n * `{ results: [{ id, extra_attributes: { … } }] }` (node-redis's default). Distance → similarity\n * via `1 - distance` (COSINE), and the `prefix` is stripped back off the key to recover the chunk id.\n */\nfunction parseSearchReply(reply: unknown, prefix: string): Passage[] {\n if (Array.isArray(reply)) {\n const passages: Passage[] = [];\n for (let index = 1; index + 1 < reply.length; index += 2) {\n const rawFields = reply[index + 1];\n if (Array.isArray(rawFields)) {\n const attrs: Record<string, string> = {};\n for (let field = 0; field + 1 < rawFields.length; field += 2) {\n attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);\n }\n passages.push(toPassage(toStr(reply[index]), attrs, prefix));\n }\n }\n return passages;\n }\n if (typeof reply === 'object' && reply !== null && 'results' in reply) {\n const results = (reply as { results: unknown }).results;\n if (Array.isArray(results)) {\n return results.map((result) => toPassage(readId(result), readAttributes(result), prefix));\n }\n }\n return [];\n}\n\nfunction readId(result: unknown): string {\n if (typeof result === 'object' && result !== null && 'id' in result) {\n return toStr((result as { id: unknown }).id);\n }\n return '';\n}\n\nfunction readAttributes(result: unknown): Record<string, string> {\n const attrs: Record<string, string> = {};\n if (typeof result === 'object' && result !== null && 'extra_attributes' in result) {\n const raw = (result as { extra_attributes: unknown }).extra_attributes;\n if (typeof raw === 'object' && raw !== null) {\n for (const [key, value] of Object.entries(raw)) {\n attrs[key] = toStr(value);\n }\n }\n }\n return attrs;\n}\n\nfunction toPassage(rawId: string, attrs: Record<string, string>, prefix: string): Passage {\n const distance = Number(attrs.vector_score ?? '1');\n const source = attrs.source;\n return {\n id: rawId.startsWith(prefix) ? rawId.slice(prefix.length) : rawId,\n text: attrs.text ?? '',\n score: 1 - distance,\n ...(source !== undefined ? { source } : {}),\n ...parseMetadata(attrs.metadata_json),\n };\n}\n\nfunction parseMetadata(raw: string | undefined): { metadata?: Record<string, unknown> } {\n if (raw === undefined) {\n return {};\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed === 'object' && parsed !== null) {\n return { metadata: parsed as Record<string, unknown> };\n }\n } catch {\n // not JSON — no metadata\n }\n return {};\n}\n","import type {\n EmbeddingProvider,\n Passage,\n RetrieveOptions,\n Retriever,\n} from '@dudousxd/nestjs-agent-core';\nimport type { VectorStore } from './vector-store.js';\n\n/**\n * Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:\n * embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool\n * via `createRetrievalTool`, or into `AgentModule.forRoot({ retrieval })` for inject mode).\n */\nexport class EmbeddingRetriever implements Retriever {\n constructor(\n private readonly embedder: EmbeddingProvider,\n private readonly store: VectorStore,\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const [embedding] = await this.embedder.embed([query]);\n if (embedding === undefined) {\n return [];\n }\n return this.store.search(embedding, {\n topK: options.topK ?? 5,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { matchesFilter } from './filter.js';\nimport type { ChunkRecord } from './ingest.js';\n\nexport interface KeywordRetrieverOptions {\n /** BM25 term-frequency saturation. Default 1.5. */\n k1?: number;\n /** BM25 length normalization, 0 (off) … 1 (full). Default 0.75. */\n b?: number;\n}\n\ninterface IndexedDoc {\n record: ChunkRecord;\n /** term → frequency within this doc. */\n frequencies: Map<string, number>;\n length: number;\n}\n\n/**\n * An in-memory BM25 lexical retriever — the keyword half of hybrid search. Feed it the SAME\n * {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids\n * line up and `HybridRetriever` can fuse the two rankings. Full BM25: idf, term-frequency saturation\n * (`k1`), and document-length normalization (`b`).\n */\nexport class KeywordRetriever implements Retriever {\n private readonly docs = new Map<string, IndexedDoc>();\n private readonly documentFrequency = new Map<string, number>();\n private totalLength = 0;\n private readonly k1: number;\n private readonly b: number;\n\n constructor(options: KeywordRetrieverOptions = {}) {\n this.k1 = options.k1 ?? 1.5;\n this.b = options.b ?? 0.75;\n }\n\n /** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */\n add(records: ChunkRecord[]): void {\n for (const record of records) {\n this.remove(record.id);\n const tokens = tokenize(record.text);\n const frequencies = new Map<string, number>();\n for (const token of tokens) {\n frequencies.set(token, (frequencies.get(token) ?? 0) + 1);\n }\n for (const term of frequencies.keys()) {\n this.documentFrequency.set(term, (this.documentFrequency.get(term) ?? 0) + 1);\n }\n this.docs.set(record.id, { record, frequencies, length: tokens.length });\n this.totalLength += tokens.length;\n }\n }\n\n private remove(id: string): void {\n const existing = this.docs.get(id);\n if (existing === undefined) {\n return;\n }\n for (const term of existing.frequencies.keys()) {\n const next = (this.documentFrequency.get(term) ?? 1) - 1;\n if (next <= 0) {\n this.documentFrequency.delete(term);\n } else {\n this.documentFrequency.set(term, next);\n }\n }\n this.totalLength -= existing.length;\n this.docs.delete(id);\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const total = this.docs.size;\n if (total === 0) {\n return [];\n }\n const averageLength = this.totalLength / total;\n const queryTerms = new Set(tokenize(query));\n const scored: Passage[] = [];\n for (const { record, frequencies, length } of this.docs.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n let score = 0;\n for (const term of queryTerms) {\n const termFrequency = frequencies.get(term);\n if (termFrequency === undefined) {\n continue;\n }\n const df = this.documentFrequency.get(term) ?? 0;\n const idf = Math.log(1 + (total - df + 0.5) / (df + 0.5));\n const denominator =\n termFrequency + this.k1 * (1 - this.b + (this.b * length) / averageLength);\n score += idf * ((termFrequency * (this.k1 + 1)) / denominator);\n }\n if (score > 0) {\n scored.push({\n id: record.id,\n text: record.text,\n score,\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK ?? 5);\n }\n}\n\nfunction tokenize(text: string): string[] {\n return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\n\nexport interface HybridRetrieverOptions {\n /** RRF constant — larger flattens the rank weighting. Default 60 (the value from the original paper). */\n k?: number;\n /** Candidates pulled from each retriever before fusion. Default 20. */\n fetchTopK?: number;\n /** Per-retriever multipliers on each RRF contribution (same order as `retrievers`). Default all 1. */\n weights?: number[];\n}\n\n/**\n * Fuses several retrievers with Reciprocal Rank Fusion — the robust way to combine, say, dense\n * vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the\n * lists it appears in, so it needs no score normalization between incompatible scales (cosine vs\n * BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.\n */\nexport class HybridRetriever implements Retriever {\n constructor(\n private readonly retrievers: Retriever[],\n private readonly options: HybridRetrieverOptions = {},\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const k = this.options.k ?? 60;\n const fetchTopK = this.options.fetchTopK ?? 20;\n const weights = this.options.weights;\n const lists = await Promise.all(\n this.retrievers.map((retriever) =>\n retriever.retrieve(query, {\n topK: fetchTopK,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n }),\n ),\n );\n\n const fused = new Map<string, { passage: Passage; score: number }>();\n lists.forEach((list, listIndex) => {\n const weight = weights?.[listIndex] ?? 1;\n list.forEach((passage, rank) => {\n const contribution = weight / (k + rank + 1);\n const existing = fused.get(passage.id);\n if (existing !== undefined) {\n existing.score += contribution;\n } else {\n fused.set(passage.id, { passage, score: contribution });\n }\n });\n });\n\n return [...fused.values()]\n .sort((a, b) => b.score - a.score)\n .slice(0, options.topK ?? 5)\n .map((entry) => ({ ...entry.passage, score: entry.score }));\n }\n}\n","import type { Passage, Reranker, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\n\nexport interface RerankingRetrieverOptions {\n /** How many candidates to pull from the base retriever before reranking. Default 20. */\n fetchTopK?: number;\n}\n\n/**\n * Two-stage retrieval: over-fetch cheap candidates from a base `Retriever`, then reorder them with a\n * stronger {@link Reranker} and keep the top few. The standard precision boost — a fast first stage\n * casts a wide net, a slow accurate second stage sharpens it. Composes over ANY retriever (vector,\n * hybrid, keyword), and is itself a `Retriever`.\n */\nexport class RerankingRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly reranker: Reranker,\n private readonly options: RerankingRetrieverOptions = {},\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const candidates = await this.base.retrieve(query, {\n topK: this.options.fetchTopK ?? 20,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n return this.reranker.rerank(query, candidates, { topK: options.topK ?? 5 });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\n\n/**\n * Wraps a `Retriever` so every `retrieve` call ANDs a fixed metadata `filter` on top of whatever the\n * caller passes — the owner/tenant-scoping primitive. `new FilteredRetriever(base, { ownerId })` only\n * ever returns that owner's passages, so a shared vector store serves per-user / per-tenant RAG\n * without the agent (or a crafted query) reaching across the boundary.\n *\n * The fixed filter takes precedence on key conflicts: a caller can narrow further but cannot widen\n * past the scope. Composes over ANY retriever (vector, hybrid, reranked) and is itself a `Retriever`.\n */\nexport class FilteredRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly filter: Record<string, unknown>,\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return this.base.retrieve(query, {\n ...options,\n filter: { ...options.filter, ...this.filter },\n });\n }\n}\n","import type { EmbeddingProvider } from '@dudousxd/nestjs-agent-core';\nimport { type ChunkOptions, chunkText } from './chunk.js';\nimport type { VectorRecord, VectorStore } from './vector-store.js';\n\n/** A source document to ingest. `id` scopes the chunk ids (`${id}#${n}`); `source` is the citation. */\nexport interface IngestDocument {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\n/** A chunked, not-yet-embedded record. Feed the same array to a vector store AND a keyword index so\n * both retrievers key on identical chunk ids (which is what lets hybrid fusion line them up). */\nexport interface ChunkRecord {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface IngestChunksOptions {\n embedder: EmbeddingProvider;\n store: VectorStore;\n}\n\nexport interface IngestOptions extends ChunkOptions, IngestChunksOptions {}\n\n/**\n * Split documents into chunk records. Chunk ids are `${doc.id}#${index}`, so re-chunking the same\n * document produces the same ids (upsert overwrites in place rather than duplicating).\n */\nexport function chunkDocuments(\n documents: IngestDocument[],\n options: ChunkOptions = {},\n): ChunkRecord[] {\n const chunks: ChunkRecord[] = [];\n for (const document of documents) {\n chunkText(document.text, options).forEach((text, index) => {\n chunks.push({\n id: `${document.id}#${index}`,\n text,\n ...(document.source !== undefined ? { source: document.source } : {}),\n ...(document.metadata !== undefined ? { metadata: document.metadata } : {}),\n });\n });\n }\n return chunks;\n}\n\n/** Embed pre-chunked records (one batched `embed`) and upsert them. Returns the record count. */\nexport async function ingestChunks(\n chunks: ChunkRecord[],\n options: IngestChunksOptions,\n): Promise<number> {\n if (chunks.length === 0) {\n return 0;\n }\n const embeddings = await options.embedder.embed(chunks.map((chunk) => chunk.text));\n const records: VectorRecord[] = chunks.map((chunk, index) => ({\n ...chunk,\n embedding: embeddings[index] ?? [],\n }));\n await options.store.upsert(records);\n return records.length;\n}\n\n/** Chunk → embed → upsert in one call. For hybrid search, use {@link chunkDocuments} +\n * {@link ingestChunks} and feed the same chunks to a {@link import('./keyword-retriever.js').KeywordRetriever}. */\nexport async function ingestDocuments(\n documents: IngestDocument[],\n options: IngestOptions,\n): Promise<number> {\n return ingestChunks(chunkDocuments(documents, options), options);\n}\n","import type { Passage, Retriever, ToolHandler, ToolSpec } from '@dudousxd/nestjs-agent-core';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\n\nexport interface RetrievalToolOptions {\n /** Tool name the model sees. Default `search_knowledge`. */\n name?: string;\n description?: string;\n /** Passages per search. Default lets the retriever decide (typically 5). */\n topK?: number;\n}\n\n/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */\nexport interface RetrievalTool {\n spec: ToolSpec;\n handler: ToolHandler;\n}\n\nfunction isQueryInput(value: unknown): value is { query: string } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'query' in value &&\n typeof (value as Record<'query', unknown>).query === 'string'\n );\n}\n\n/** A hand-rolled Standard Schema for `{ query: string }` — keeps this package validation-lib-free. */\nconst querySchema: StandardSchemaV1<{ query: string }> = {\n '~standard': {\n version: 1,\n vendor: 'nestjs-agent-rag',\n validate(value: unknown) {\n return isQueryInput(value)\n ? { value: { query: value.query } }\n : { issues: [{ message: 'retrieval tool expects { query: string }' }] };\n },\n },\n};\n\n/**\n * Builds the agentic-retrieval tool: a `read`-kind `search_knowledge(query)` the model calls when it\n * decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources\n * flow to the frontend and telescope through the normal tool-call machinery (the citation surface).\n *\n * ```ts\n * providers: [provideAgentTool(createRetrievalTool(retriever))]\n * ```\n */\nexport function createRetrievalTool(\n retriever: Retriever,\n options: RetrievalToolOptions = {},\n): RetrievalTool {\n const topK = options.topK;\n return {\n spec: {\n name: options.name ?? 'search_knowledge',\n kind: 'read',\n description:\n options.description ??\n 'Search the knowledge base for passages relevant to a query. Returns passages with their source for citation.',\n inputSchema: querySchema,\n },\n handler: {\n async execute(input: unknown): Promise<{ passages: Passage[] }> {\n if (!isQueryInput(input)) {\n throw new Error('retrieval tool expects { query: string }');\n }\n const passages = await retriever.retrieve(input.query, topK !== undefined ? { topK } : {});\n return { passages };\n },\n },\n };\n}\n"],"mappings":"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","names":["chunkText","text","options","chunkSize","overlap","normalized","trim","length","chunks","start","end","Math","min","breakAt","window","slice","boundary","max","lastIndexOf","chunk","push","documentIdOf","chunkId","replace","matchesFilter","metadata","filter","entries","Object","undefined","length","every","key","expected","Array","isArray","actual","some","value","includes","MemoryVectorStore","records","Map","upsert","record","set","id","remove","documentId","keys","documentIdOf","delete","listDocuments","filter","documents","values","undefined","matchesFilter","metadata","has","search","embedding","options","scored","push","text","score","cosineSimilarity","source","sort","a","b","slice","topK","dot","normA","normB","length","Math","min","index","valueA","valueB","denominator","sqrt","PgVectorStore","table","dimensions","client","options","ensureSchema","query","upsert","records","record","id","text","source","metadata","undefined","JSON","stringify","toVectorLiteral","embedding","remove","documentId","DOCUMENT_ID_FROM_CHUNK","listDocuments","filter","where","buildWhere","rows","sql","params","map","row","doc_id","search","vector","topK","score","Number","startIndex","Object","keys","length","clauses","scalar","index","key","value","entries","Array","isArray","push","keyParam","arrParam","String","join","RedisVectorStore","index","prefix","dimensions","filterableFields","client","options","ensureSchema","sendCommand","schema","field","push","String","upsert","records","record","args","id","text","encodeVector","embedding","source","undefined","metadata","JSON","stringify","value","tag","Array","isArray","map","join","remove","documentId","keys","Set","pattern","escapeGlob","cursor","reply","page","parseScanReply","key","add","size","listDocuments","filter","filterMatchesNothing","query","buildFilter","documents","Map","batchSize","offset","passages","parseSearchReply","passage","documentIdOf","has","set","length","values","search","topK","Buffer","from","Float32Array","buffer","Object","clauses","entries","alternation","entry","escapeTag","some","replace","char","rawKeys","toStr","isBuffer","toString","rawFields","attrs","toPassage","results","result","readId","readAttributes","raw","extra_attributes","rawId","distance","Number","vector_score","startsWith","slice","score","parseMetadata","metadata_json","parsed","parse","EmbeddingRetriever","embedder","store","retrieve","query","options","embedding","embed","undefined","search","topK","filter","KeywordRetriever","docs","Map","documentFrequency","totalLength","k1","b","options","add","records","record","remove","id","tokens","tokenize","text","frequencies","token","set","get","term","keys","length","existing","undefined","next","delete","retrieve","query","total","size","averageLength","queryTerms","Set","scored","values","filter","matchesFilter","metadata","score","termFrequency","df","idf","Math","log","denominator","push","source","sort","a","slice","topK","toLowerCase","match","HybridRetriever","retrievers","options","retrieve","query","k","fetchTopK","weights","lists","Promise","all","map","retriever","topK","filter","undefined","fused","Map","forEach","list","listIndex","weight","passage","rank","contribution","existing","get","id","score","set","values","sort","a","b","slice","entry","RerankingRetriever","base","reranker","options","retrieve","query","candidates","topK","fetchTopK","filter","undefined","rerank","FilteredRetriever","base","filter","retrieve","query","options","chunkDocuments","documents","options","chunks","document","chunkText","text","forEach","index","push","id","source","undefined","metadata","ingestChunks","length","embeddings","embedder","embed","map","chunk","records","embedding","store","upsert","ingestDocuments","isQueryInput","value","query","querySchema","version","vendor","validate","issues","message","createRetrievalTool","retriever","options","topK","spec","name","kind","description","inputSchema","handler","execute","input","Error","passages","retrieve","undefined"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/chunk.ts","../src/vector-store.ts","../src/filter.ts","../src/memory-vector-store.ts","../src/pg-vector-store.ts","../src/redis-vector-store.ts","../src/embedding-retriever.ts","../src/keyword-retriever.ts","../src/lexical-retriever.ts","../src/hybrid-retriever.ts","../src/reranking-retriever.ts","../src/filtered-retriever.ts","../src/ingest.ts","../src/retrieval-tool.ts"],"sourcesContent":["export { chunkText, type ChunkOptions } from './chunk.js';\nexport type {\n IndexedDocument,\n LexicalVectorStore,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\nexport { documentIdOf, isLexicalVectorStore } from './vector-store.js';\nexport { MemoryVectorStore } from './memory-vector-store.js';\nexport {\n PgVectorStore,\n type PgClient,\n type PgVectorStoreOptions,\n} from './pg-vector-store.js';\nexport {\n RedisVectorStore,\n RedisVectorSchemaMismatchError,\n type RedisSearchClient,\n type RedisVectorStoreOptions,\n} from './redis-vector-store.js';\nexport { EmbeddingRetriever } from './embedding-retriever.js';\nexport { KeywordRetriever, type KeywordRetrieverOptions } from './keyword-retriever.js';\nexport { LexicalRetriever } from './lexical-retriever.js';\nexport { HybridRetriever, type HybridRetrieverOptions } from './hybrid-retriever.js';\nexport { RerankingRetriever, type RerankingRetrieverOptions } from './reranking-retriever.js';\nexport { FilteredRetriever } from './filtered-retriever.js';\nexport {\n chunkDocuments,\n ingestChunks,\n ingestDocuments,\n type ChunkRecord,\n type IngestChunksOptions,\n type IngestDocument,\n type IngestOptions,\n} from './ingest.js';\nexport {\n createRetrievalTool,\n type RetrievalTool,\n type RetrievalToolOptions,\n} from './retrieval-tool.js';\n","export interface ChunkOptions {\n /** Target max characters per chunk. Default 800. */\n chunkSize?: number;\n /** Characters of overlap carried from the end of one chunk into the next. Default 100. */\n overlap?: number;\n}\n\n/**\n * Splits text into overlapping chunks for embedding. Greedy up to `chunkSize`, but prefers to break\n * on a paragraph → sentence → word boundary in the back half of the window (so a chunk rarely cuts\n * mid-sentence), then carries `overlap` characters into the next chunk to preserve context across\n * the seam. Pure and deterministic.\n */\nexport function chunkText(text: string, options: ChunkOptions = {}): string[] {\n const chunkSize = options.chunkSize ?? 800;\n const overlap = options.overlap ?? 100;\n const normalized = text.trim();\n if (normalized.length === 0) {\n return [];\n }\n if (normalized.length <= chunkSize) {\n return [normalized];\n }\n\n const chunks: string[] = [];\n let start = 0;\n while (start < normalized.length) {\n const end = Math.min(start + chunkSize, normalized.length);\n let breakAt = end;\n if (end < normalized.length) {\n const window = normalized.slice(start, end);\n const boundary = Math.max(\n window.lastIndexOf('\\n\\n'),\n window.lastIndexOf('. '),\n window.lastIndexOf(' '),\n );\n // Only honour a boundary in the back half, else a tiny chunk cascades.\n if (boundary > chunkSize / 2) {\n breakAt = start + boundary + 1;\n }\n }\n const chunk = normalized.slice(start, breakAt).trim();\n if (chunk.length > 0) {\n chunks.push(chunk);\n }\n if (breakAt >= normalized.length) {\n break;\n }\n // Step forward with overlap, but always make progress (guards against a stuck boundary).\n start = Math.max(breakAt - overlap, start + 1);\n }\n return chunks;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\n\n/** A stored, embedded chunk. `embedding` length must match the store's configured dimensions. */\nexport interface VectorRecord {\n id: string;\n text: string;\n embedding: number[];\n /** Citation-facing origin (document title, URL, row id). */\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface VectorSearchOptions {\n topK: number;\n /** Exact-match metadata filter (all keys must equal). Passed through from `RetrieveOptions.filter`. */\n filter?: Record<string, unknown>;\n}\n\n/**\n * The write + search side of RAG storage. `@dudousxd/nestjs-agent-rag` ships `MemoryVectorStore`\n * (in-JS cosine, tests + small scale) and `PgVectorStore` (pgvector); any impl works. Pair one with\n * an `EmbeddingProvider` via {@link import('./embedding-retriever.js').EmbeddingRetriever} to get a\n * core `Retriever`.\n */\nexport interface VectorStore {\n upsert(records: VectorRecord[]): Promise<void>;\n search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;\n /**\n * Delete every chunk belonging to a source document — all records whose id is `${documentId}`\n * or `${documentId}#<n>` (the id scheme {@link import('./ingest.js').chunkDocuments} produces).\n * Use it to drop a document from the index, and before re-ingesting one: `upsert` overwrites\n * matching ids but can't remove chunks a shorter new version no longer produces, so a re-ingest\n * without a preceding `remove` leaves the old tail orphaned.\n */\n remove(documentId: string): Promise<void>;\n /**\n * List the distinct source documents currently indexed (chunk ids collapsed back to their document\n * by stripping the trailing `#<n>`), each with a representative chunk's `metadata`, optionally\n * narrowed by a metadata `filter`. The enumeration seam for reconciliation: diff this against your\n * source of truth to find documents to ingest (missing), re-ingest (a stored fingerprint like\n * `size` changed), or {@link VectorStore.remove remove} (orphaned).\n */\n listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;\n}\n\n/**\n * An **optional** capability a {@link VectorStore} may also implement: full-text (lexical) search\n * over the chunk text it already stores, using the backing engine's own keyword index — RediSearch's\n * BM25 over the `TEXT` field, say. Its point is the multi-process deployment: when ingestion runs on\n * a worker and search runs on an API pod, the API process never sees the chunks, so the in-process\n * {@link import('./keyword-retriever.js').KeywordRetriever} has nothing to index. A store that\n * implements this needs no second index, no corpus in JS heap, and no refresh window — a chunk is\n * lexically findable the moment it is upserted.\n *\n * Wrap it as a core `Retriever` with {@link import('./lexical-retriever.js').LexicalRetriever}.\n *\n * Implementations MUST apply `options.filter` with exactly the same semantics as\n * {@link VectorStore.search} — including the empty-array deny primitive — because the filter is\n * routinely an access-control boundary, not a hint.\n */\nexport interface LexicalVectorStore extends VectorStore {\n /**\n * Rank stored chunks against a natural-language `query` by keyword relevance. `options.filter`\n * narrows exactly as in {@link VectorStore.search}. The returned `score` is the engine's own\n * relevance score (BM25), which shares no scale with `search`'s cosine similarity — fuse the two\n * with {@link import('./hybrid-retriever.js').HybridRetriever}, whose RRF is rank-based and so\n * needs no common scale.\n */\n searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;\n}\n\n/**\n * Does this store carry the optional {@link LexicalVectorStore} capability? Lets a consumer pick the\n * store-backed lexical leg when available and fall back to `KeywordRetriever` (or to dense-only)\n * when it is not, without knowing which adapter it was handed.\n */\nexport function isLexicalVectorStore(store: VectorStore): store is LexicalVectorStore {\n return typeof (store as Partial<LexicalVectorStore>).searchText === 'function';\n}\n\n/** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */\nexport interface IndexedDocument {\n id: string;\n metadata?: Record<string, unknown>;\n}\n\n/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */\nexport function documentIdOf(chunkId: string): string {\n return chunkId.replace(/#\\d+$/, '');\n}\n","/** Metadata filter, shared by the in-memory {@link import('./memory-vector-store.js').MemoryVectorStore}\n * and {@link import('./keyword-retriever.js').KeywordRetriever}. Every key must match:\n *\n * - a **scalar** filter value keeps exact-match — the record's value must equal it;\n * - an **array** filter value means **match-any** (OR / set membership) — the record matches when its\n * value for that key is one of them, or (for a multi-valued record) shares at least one element.\n * An empty array matches nothing (no value is a member of the empty set) — the deny primitive.\n *\n * Array-valued filters are how a caller expresses capability-style access control over a shared store\n * (e.g. `{ audience: ['public', 'role:ADMIN', 'base:…'] }`) without the store knowing what a token means.\n */\nexport function matchesFilter(\n metadata: Record<string, unknown> | undefined,\n filter: Record<string, unknown>,\n): boolean {\n const entries = Object.entries(filter);\n if (metadata === undefined) {\n return entries.length === 0;\n }\n return entries.every(([key, expected]) => {\n if (Array.isArray(expected)) {\n const actual = metadata[key];\n return Array.isArray(actual)\n ? actual.some((value) => expected.includes(value))\n : expected.includes(actual);\n }\n return metadata[key] === expected;\n });\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport { matchesFilter } from './filter.js';\nimport {\n type IndexedDocument,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter\n * for tests and small/embedded corpora; for production scale use `PgVectorStore` (or your own).\n */\nexport class MemoryVectorStore implements VectorStore {\n private readonly records = new Map<string, VectorRecord>();\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n this.records.set(record.id, record);\n }\n }\n\n async remove(documentId: string): Promise<void> {\n for (const id of this.records.keys()) {\n if (documentIdOf(id) === documentId) {\n this.records.delete(id);\n }\n }\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const documents = new Map<string, IndexedDocument>();\n for (const record of this.records.values()) {\n if (filter !== undefined && !matchesFilter(record.metadata, filter)) {\n continue;\n }\n const id = documentIdOf(record.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return [...documents.values()];\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n const scored: Passage[] = [];\n for (const record of this.records.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n scored.push({\n id: record.id,\n text: record.text,\n score: cosineSimilarity(embedding, record.embedding),\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK);\n }\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n let dot = 0;\n let normA = 0;\n let normB = 0;\n const length = Math.min(a.length, b.length);\n for (let index = 0; index < length; index += 1) {\n const valueA = a[index] ?? 0;\n const valueB = b[index] ?? 0;\n dot += valueA * valueB;\n normA += valueA * valueA;\n normB += valueB * valueB;\n }\n const denominator = Math.sqrt(normA) * Math.sqrt(normB);\n return denominator === 0 ? 0 : dot / denominator;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport type {\n IndexedDocument,\n VectorRecord,\n VectorSearchOptions,\n VectorStore,\n} from './vector-store.js';\n\n/**\n * The minimal Postgres surface {@link PgVectorStore} needs — adapt your own `pg` / `postgres.js`\n * client to it, so this package pulls in NO driver (bring your own, like the store adapters take an\n * ORM handle and the Redis transport takes a client). `query` runs parameterized SQL (`$1,$2,…`) and\n * resolves the result rows.\n */\nexport interface PgClient {\n query<Row = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<Row[]>;\n}\n\nexport interface PgVectorStoreOptions {\n /** Table name. Default `agent_rag_chunks`. */\n table?: string;\n /** Embedding width — must match your model (e.g. 1536 for text-embedding-3-small). Default 1536. */\n dimensions?: number;\n}\n\n/**\n * A pgvector-backed {@link VectorStore} — the production reference adapter. Cosine distance via the\n * `<=>` operator over an HNSW index; metadata in a `jsonb` column filtered with `@>`. Call\n * {@link PgVectorStore.ensureSchema} once at boot to create the extension, table, and index.\n *\n * This store does **not** implement the optional\n * {@link import('./vector-store.js').LexicalVectorStore} capability, unlike\n * {@link import('./redis-vector-store.js').RedisVectorStore}. That is a deliberate omission, not an\n * oversight: RediSearch already indexes the chunk text (the schema declares it `TEXT`), so BM25 is\n * free there, whereas the table below indexes nothing lexically — `text` is a plain column with no\n * `tsvector` and no GIN index. Adding one is not a transparent upgrade:\n *\n * - it is new DDL in `ensureSchema`, and `CREATE INDEX` (non-concurrently, as this method must, since\n * `CONCURRENTLY` cannot run in a transaction) takes a write lock on an already-populated chunks\n * table — a boot-time stall proportional to corpus size for every existing deployment;\n * - it forces a text-search-configuration choice (`english` vs `simple` vs …) that must match between\n * the index expression and every query, or Postgres silently ignores the index and sequentially\n * scans the corpus — a failure mode that stays *correct* while quietly getting slower with scale.\n *\n * Both want a migration a consumer runs and observes, not a side effect of upgrading the library. The\n * capability interface is open, so a `PgLexicalVectorStore` (or an option here, gated on an\n * explicitly-created index) can be added later without changing anything else.\n */\nexport class PgVectorStore implements VectorStore {\n private readonly table: string;\n private readonly dimensions: number;\n\n constructor(\n private readonly client: PgClient,\n options: PgVectorStoreOptions = {},\n ) {\n this.table = options.table ?? 'agent_rag_chunks';\n this.dimensions = options.dimensions ?? 1536;\n }\n\n /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */\n async ensureSchema(): Promise<void> {\n await this.client.query('CREATE EXTENSION IF NOT EXISTS vector');\n await this.client.query(\n `CREATE TABLE IF NOT EXISTS ${this.table} (\n id TEXT PRIMARY KEY,\n text TEXT NOT NULL,\n source TEXT,\n metadata JSONB,\n embedding vector(${this.dimensions}) NOT NULL\n )`,\n );\n await this.client.query(\n `CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx\n ON ${this.table} USING hnsw (embedding vector_cosine_ops)`,\n );\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n await this.client.query(\n `INSERT INTO ${this.table} (id, text, source, metadata, embedding)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (id) DO UPDATE SET\n text = EXCLUDED.text,\n source = EXCLUDED.source,\n metadata = EXCLUDED.metadata,\n embedding = EXCLUDED.embedding`,\n [\n record.id,\n record.text,\n record.source ?? null,\n record.metadata !== undefined ? JSON.stringify(record.metadata) : null,\n toVectorLiteral(record.embedding),\n ],\n );\n }\n }\n\n async remove(documentId: string): Promise<void> {\n await this.client.query(`DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1`, [\n documentId,\n ]);\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n const where = buildWhere(filter, 1);\n // DISTINCT ON collapses chunks to one row per document; all chunks share the doc's metadata.\n const rows = await this.client.query<{\n doc_id: string;\n metadata: Record<string, unknown> | null;\n }>(\n `SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata\n FROM ${this.table}\n ${where.sql}\n ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`,\n where.params,\n );\n return rows.map((row) => ({\n id: row.doc_id,\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n const vector = toVectorLiteral(embedding);\n // $1 = query vector, $2 = topK; metadata-filter params start at $3.\n const where = buildWhere(options.filter, 3);\n const rows = await this.client.query<PgRow>(\n `SELECT id, text, source, metadata, 1 - (embedding <=> $1::vector) AS score\n FROM ${this.table}\n ${where.sql}\n ORDER BY embedding <=> $1::vector\n LIMIT $2`,\n [vector, options.topK, ...where.params],\n );\n return rows.map((row) => ({\n id: row.id,\n text: row.text,\n score: Number(row.score),\n ...(row.source !== null ? { source: row.source } : {}),\n ...(row.metadata !== null ? { metadata: row.metadata } : {}),\n }));\n }\n}\n\ninterface PgRow {\n id: string;\n text: string;\n source: string | null;\n metadata: Record<string, unknown> | null;\n score: number | string;\n}\n\n/**\n * Build the `WHERE` fragment for a metadata `filter`, with parameter placeholders numbered from\n * `startIndex`. Scalar values collapse into a single `@>` jsonb-containment check (exact-match, as\n * before). An **array** value is a **match-any** (OR / set membership) check via jsonb `?|`: the\n * record matches when its value for that key — scalar or array — shares an element with the filter\n * array. An empty array can never match (deny primitive). Keys are passed as parameters (`metadata->$k`)\n * so a caller-supplied metadata key can't inject SQL. Returns `{ sql: '', params: [] }` when there is\n * no filter, preserving the previous unfiltered query shape.\n */\nfunction buildWhere(\n filter: Record<string, unknown> | undefined,\n startIndex: number,\n): { sql: string; params: unknown[] } {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return { sql: '', params: [] };\n }\n const clauses: string[] = [];\n const params: unknown[] = [];\n const scalar: Record<string, unknown> = {};\n let index = startIndex;\n for (const [key, value] of Object.entries(filter)) {\n if (Array.isArray(value)) {\n if (value.length === 0) {\n clauses.push('false');\n continue;\n }\n const keyParam = `$${index}`;\n params.push(key);\n index += 1;\n const arrParam = `$${index}`;\n params.push(value.map(String));\n index += 1;\n clauses.push(\n `(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' ` +\n `THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`,\n );\n } else {\n scalar[key] = value;\n }\n }\n if (Object.keys(scalar).length > 0) {\n clauses.push(`metadata @> $${index}::jsonb`);\n params.push(JSON.stringify(scalar));\n index += 1;\n }\n return { sql: `WHERE ${clauses.join(' AND ')}`, params };\n}\n\n/**\n * SQL that collapses a chunk id (`${documentId}#<n>`) back to its source document id — the pgvector\n * mirror of {@link import('./vector-store.js').documentIdOf}. Shared by `remove` and\n * `listDocuments` so both key on the exact same definition of \"chunk belongs to document\".\n */\nconst DOCUMENT_ID_FROM_CHUNK = \"regexp_replace(id, '#[0-9]+$', '')\";\n\n/** pgvector accepts a `'[1,2,3]'` text literal cast to `vector`. */\nfunction toVectorLiteral(embedding: number[]): string {\n return `[${embedding.join(',')}]`;\n}\n","import type { Passage } from '@dudousxd/nestjs-agent-core';\nimport {\n type IndexedDocument,\n type LexicalVectorStore,\n type VectorRecord,\n type VectorSearchOptions,\n type VectorStore,\n documentIdOf,\n} from './vector-store.js';\n\n/**\n * The minimal Redis surface {@link RedisVectorStore} needs: send a raw command and get the reply.\n * Both `node-redis` (`client.sendCommand(args)`) and `ioredis` (wrap `redis.call`) satisfy it — so\n * this package pulls in NO Redis driver (bring your own, like the other adapters). Requires the\n * RediSearch module (Redis Stack / Redis 8+).\n */\nexport interface RedisSearchClient {\n sendCommand(args: (string | Buffer)[]): Promise<unknown>;\n}\n\n/**\n * The index that already exists in Redis contradicts this store's configuration in a way that cannot\n * be repaired in place. Thrown by {@link RedisVectorStore.ensureSchema} — which would otherwise have\n * returned happily and left every subsequent query quietly wrong.\n *\n * The canonical case is a changed embedding width (a model swap, 1024 → 1536): RediSearch fixes a\n * vector field's `DIM` at `FT.CREATE`, and every vector already stored was produced by the OLD model,\n * so there is no in-place fix — the index has to be dropped and the corpus re-embedded. That is\n * destructive and belongs to the host, never to a boot-time `ensureSchema`.\n */\nexport class RedisVectorSchemaMismatchError extends Error {\n constructor(\n /** The RediSearch index that was inspected. */\n readonly index: string,\n /** The index attribute that differs (e.g. `embedding`, `meta_tenant`). */\n readonly field: string,\n /** What this store is configured for. */\n readonly expected: string,\n /** What `FT.INFO` reports the live index has. */\n readonly actual: string,\n remedy: string,\n ) {\n super(\n `RediSearch index \"${index}\" disagrees with this RedisVectorStore's configuration: ` +\n `field \"${field}\" is ${actual}, expected ${expected}. ${remedy}`,\n );\n this.name = 'RedisVectorSchemaMismatchError';\n }\n}\n\nexport interface RedisVectorStoreOptions {\n /** RediSearch index name. Default `agent_rag_idx`. */\n index?: string;\n /** Key prefix for the per-chunk hashes. Default `agent_rag:`. */\n prefix?: string;\n /** Embedding width — must match your model (e.g. 1536). Default 1536. */\n dimensions?: number;\n /**\n * Metadata keys to index as filterable TAG fields. RediSearch needs filter fields declared up\n * front, so only keys listed here can be passed in `search`'s `filter`. Default none.\n */\n filterableFields?: string[];\n /**\n * RediSearch scorer used by {@link RedisVectorStore.searchText}. Default `BM25` — the standard\n * lexical ranking, and the same family the in-process `KeywordRetriever` implements. Override for\n * a server that names it differently (`BM25STD` on newer builds) or to pick `TFIDF`/`DISMAX`.\n */\n lexicalScorer?: string;\n}\n\n/**\n * A RediSearch-backed {@link VectorStore} — HNSW + cosine over Redis, the ecosystem-native option\n * for anyone already running Redis (see `-transport-redis`). Chunks are hashes under `prefix`, the\n * embedding a FLOAT32 vector field; `search` is a KNN query. Call {@link RedisVectorStore.ensureSchema}\n * once at boot to create the index.\n *\n * The chunk text is declared as a RediSearch `TEXT` field, so this store also satisfies\n * {@link LexicalVectorStore}: {@link RedisVectorStore.searchText} is BM25 over that same index —\n * the lexical half of hybrid search with no second index to build, feed, or invalidate.\n */\nexport class RedisVectorStore implements LexicalVectorStore {\n private readonly index: string;\n private readonly prefix: string;\n private readonly dimensions: number;\n private readonly filterableFields: string[];\n private readonly lexicalScorer: string;\n\n constructor(\n private readonly client: RedisSearchClient,\n options: RedisVectorStoreOptions = {},\n ) {\n this.index = options.index ?? 'agent_rag_idx';\n this.prefix = options.prefix ?? 'agent_rag:';\n this.dimensions = options.dimensions ?? 1536;\n this.filterableFields = options.filterableFields ?? [];\n this.lexicalScorer = options.lexicalScorer ?? 'BM25';\n }\n\n /**\n * Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —\n * and if it DOES exist, compare what's there against this store's configuration instead of\n * assuming they agree. An index that outlives a config change is the normal case (schemas live in\n * Redis, config lives in a redeployed process), and the two failure modes it produces are both\n * invisible at runtime: a `filterableFields` entry added later has no `meta_*` TAG, so every search\n * filtering on it matches *nothing*; a changed `dimensions` leaves the index on the old width, so\n * writes are rejected or KNN ranks garbage. Neither raises anything on its own.\n *\n * The two are repaired differently, because only one of them CAN be repaired:\n *\n * - **A missing filterable TAG is additive** → `FT.ALTER … SCHEMA ADD`. Nothing that already works\n * changes, so doing it automatically is safe. Note the chunks written *before* the alter carry no\n * `meta_*` hash field at all (`upsert` only writes the fields configured at the time), so they\n * only become filterable on that key once they're re-ingested — the schema is repaired here, the\n * backfill is the host's call.\n * - **A dimension (or field-type) change is not repairable** → {@link RedisVectorSchemaMismatchError}.\n * It needs a drop + full reindex, which would destroy the host's corpus; failing loudly at boot is\n * the only honest option.\n *\n * If `FT.INFO` comes back in a shape this parser doesn't recognise, no drift is inferred — an\n * unreadable reply must not be turned into a false alarm that blocks a boot.\n */\n async ensureSchema(): Promise<void> {\n let info: unknown;\n try {\n info = await this.client.sendCommand(['FT.INFO', this.index]);\n } catch {\n await this.createIndex();\n return;\n }\n await this.reconcileSchema(info);\n }\n\n /** Repair what can be repaired in place; throw on what can't. */\n private async reconcileSchema(info: unknown): Promise<void> {\n const attributes = parseIndexAttributes(info);\n if (attributes.size === 0) {\n return;\n }\n\n const vector = attributes.get('embedding');\n if (vector?.dimensions !== undefined && vector.dimensions !== this.dimensions) {\n throw new RedisVectorSchemaMismatchError(\n this.index,\n 'embedding',\n `DIM ${this.dimensions}`,\n `DIM ${vector.dimensions}`,\n `A vector field's width is fixed at FT.CREATE and every stored embedding has the old one, so this cannot be altered in place: drop the index (FT.DROPINDEX ${this.index} DD) and re-ingest with the new embedding model.`,\n );\n }\n\n const missing: string[] = [];\n for (const field of this.filterableFields) {\n const name = `meta_${field}`;\n const existing = attributes.get(name);\n if (existing === undefined) {\n missing.push(name);\n } else if (existing.type !== 'TAG') {\n throw new RedisVectorSchemaMismatchError(\n this.index,\n name,\n 'TAG',\n existing.type,\n `A field's type cannot be changed in place: drop the index (FT.DROPINDEX ${this.index} DD) and re-ingest, or rename the metadata key.`,\n );\n }\n }\n for (const name of missing) {\n await this.client.sendCommand(['FT.ALTER', this.index, 'SCHEMA', 'ADD', name, 'TAG']);\n }\n }\n\n private async createIndex(): Promise<void> {\n const schema: string[] = ['text', 'TEXT', 'source', 'TAG', 'metadata_json', 'TEXT', 'NOINDEX'];\n for (const field of this.filterableFields) {\n schema.push(`meta_${field}`, 'TAG');\n }\n schema.push(\n 'embedding',\n 'VECTOR',\n 'HNSW',\n '6',\n 'TYPE',\n 'FLOAT32',\n 'DIM',\n String(this.dimensions),\n 'DISTANCE_METRIC',\n 'COSINE',\n );\n await this.client.sendCommand([\n 'FT.CREATE',\n this.index,\n 'ON',\n 'HASH',\n 'PREFIX',\n '1',\n this.prefix,\n 'SCHEMA',\n ...schema,\n ]);\n }\n\n async upsert(records: VectorRecord[]): Promise<void> {\n for (const record of records) {\n const args: (string | Buffer)[] = [\n 'HSET',\n `${this.prefix}${record.id}`,\n 'text',\n record.text,\n 'embedding',\n encodeVector(record.embedding),\n ];\n if (record.source !== undefined) {\n args.push('source', record.source);\n }\n if (record.metadata !== undefined) {\n args.push('metadata_json', JSON.stringify(record.metadata));\n for (const field of this.filterableFields) {\n const value = record.metadata[field];\n if (value !== undefined) {\n // Array metadata is stored as a multi-valued TAG (comma-separated, RediSearch's default\n // separator) so a document can carry several capability tokens; individual values must\n // therefore not contain commas.\n const tag = Array.isArray(value) ? value.map(String).join(',') : String(value);\n args.push(`meta_${field}`, tag);\n }\n }\n }\n await this.client.sendCommand(args);\n }\n }\n\n async remove(documentId: string): Promise<void> {\n const keys = new Set<string>([`${this.prefix}${documentId}`]);\n const pattern = `${this.prefix}${escapeGlob(documentId)}#*`;\n let cursor = '0';\n do {\n const reply = await this.client.sendCommand([\n 'SCAN',\n cursor,\n 'MATCH',\n pattern,\n 'COUNT',\n '256',\n ]);\n const page = parseScanReply(reply);\n cursor = page.cursor;\n for (const key of page.keys) {\n keys.add(key);\n }\n } while (cursor !== '0');\n if (keys.size > 0) {\n await this.client.sendCommand(['DEL', ...keys]);\n }\n }\n\n async listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]> {\n if (filterMatchesNothing(filter)) {\n return [];\n }\n const query = buildFilter(filter);\n const documents = new Map<string, IndexedDocument>();\n const batchSize = 1000;\n let offset = 0;\n for (;;) {\n // RETURN metadata_json (not NOCONTENT) so we can carry a representative chunk's metadata;\n // parseSearchReply already yields prefix-stripped ids + parsed metadata for both RESP2/RESP3.\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'RETURN',\n '1',\n 'metadata_json',\n 'LIMIT',\n String(offset),\n String(batchSize),\n 'DIALECT',\n '2',\n ]);\n const passages = parseSearchReply(reply, this.prefix);\n for (const passage of passages) {\n const id = documentIdOf(passage.id);\n if (!documents.has(id)) {\n documents.set(id, {\n id,\n ...(passage.metadata !== undefined ? { metadata: passage.metadata } : {}),\n });\n }\n }\n if (passages.length < batchSize) {\n break;\n }\n offset += batchSize;\n }\n return [...documents.values()];\n }\n\n async search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter)) {\n return [];\n }\n const query = `${buildFilter(options.filter)}=>[KNN ${options.topK} @embedding $BLOB AS vector_score]`;\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n query,\n 'PARAMS',\n '2',\n 'BLOB',\n encodeVector(embedding),\n 'RETURN',\n '4',\n 'text',\n 'source',\n 'metadata_json',\n 'vector_score',\n 'SORTBY',\n 'vector_score',\n 'ASC',\n 'DIALECT',\n '2',\n 'LIMIT',\n '0',\n String(options.topK),\n ]);\n return parseSearchReply(reply, this.prefix);\n }\n\n /**\n * Lexical (BM25) search over the same index `search` uses — the {@link LexicalVectorStore} half of\n * hybrid retrieval, served by RediSearch's own keyword index rather than an in-process copy of the\n * corpus. `options.filter` is ANDed onto the text clause with the identical TAG semantics as\n * `search` (array = OR, empty array = deny), so a lexical hit can never escape a filter a vector\n * hit would have respected.\n *\n * A query with no searchable terms — empty, whitespace, or pure punctuation — returns `[]`. That is\n * deliberate: RediSearch's `*` would match the entire corpus, which is the opposite of what a\n * relevance query with nothing to be relevant to should do.\n */\n async searchText(query: string, options: VectorSearchOptions): Promise<Passage[]> {\n if (filterMatchesNothing(options.filter)) {\n return [];\n }\n const terms = tokenizeQuery(query);\n if (terms.length === 0) {\n return [];\n }\n const filterQuery = buildFilter(options.filter);\n const textQuery = `@text:(${terms.join('|')})`;\n const reply = await this.client.sendCommand([\n 'FT.SEARCH',\n this.index,\n filterQuery === '*' ? textQuery : `(${filterQuery} ${textQuery})`,\n 'SCORER',\n this.lexicalScorer,\n 'WITHSCORES',\n 'RETURN',\n '3',\n 'text',\n 'source',\n 'metadata_json',\n 'DIALECT',\n '2',\n 'LIMIT',\n '0',\n String(options.topK),\n ]);\n return parseSearchReply(reply, this.prefix, 'lexical');\n }\n}\n\n/** One attribute as the live index reports it — enough to spot the drift that matters. */\ninterface IndexAttribute {\n /** `TEXT` / `TAG` / `VECTOR`. */\n type: string;\n /** `VECTOR` fields only: the width the index was created with. */\n dimensions?: number;\n}\n\n/**\n * Parse `FT.INFO`'s `attributes` into `attribute name → { type, dimensions }`, tolerating both wire\n * shapes exactly like {@link parseSearchReply} does: the RESP2 flat key/value array (whose attributes\n * are themselves flat key/value arrays) and node-redis's RESP3 object (attributes as objects).\n * Unknown shapes yield an empty map, which callers read as \"can't tell\" rather than \"nothing there\".\n */\nfunction parseIndexAttributes(reply: unknown): Map<string, IndexAttribute> {\n const attributes = new Map<string, IndexAttribute>();\n for (const entry of readInfoField(reply, 'attributes')) {\n const fields = readKeyValues(entry);\n const name = fields.attribute ?? fields.identifier;\n if (name === undefined) {\n continue;\n }\n const dim = Number(fields.dim);\n attributes.set(name, {\n type: (fields.type ?? '').toUpperCase(),\n ...(fields.dim !== undefined && Number.isFinite(dim) ? { dimensions: dim } : {}),\n });\n }\n return attributes;\n}\n\n/** Read one top-level `FT.INFO` field as an array, from either the RESP2 pair-array or RESP3 object. */\nfunction readInfoField(reply: unknown, key: string): unknown[] {\n if (Array.isArray(reply)) {\n for (let index = 0; index + 1 < reply.length; index += 2) {\n if (toStr(reply[index]) === key) {\n const value = reply[index + 1];\n return Array.isArray(value) ? value : [];\n }\n }\n return [];\n }\n if (typeof reply === 'object' && reply !== null && key in reply) {\n const value = (reply as Record<string, unknown>)[key];\n return Array.isArray(value) ? value : [];\n }\n return [];\n}\n\n/** Flatten one attribute entry (RESP2 pair-array or RESP3 object) into lower-cased string fields. */\nfunction readKeyValues(entry: unknown): Record<string, string | undefined> {\n const fields: Record<string, string | undefined> = {};\n if (Array.isArray(entry)) {\n for (let index = 0; index + 1 < entry.length; index += 2) {\n fields[toStr(entry[index]).toLowerCase()] = toStr(entry[index + 1]);\n }\n return fields;\n }\n if (typeof entry === 'object' && entry !== null) {\n for (const [key, value] of Object.entries(entry)) {\n fields[key.toLowerCase()] = toStr(value);\n }\n }\n return fields;\n}\n\n/** RediSearch expects a little-endian FLOAT32 buffer for the query/stored vector. */\nfunction encodeVector(embedding: number[]): Buffer {\n return Buffer.from(new Float32Array(embedding).buffer);\n}\n\n/**\n * `*` for no filter, else an AND of TAG clauses. A scalar value is an exact tag match\n * (`@meta_owner:{u1}`); an **array** value is a TAG alternation — match-any / OR\n * (`@meta_audience:{public|role\\:ADMIN}`), the capability-ACL primitive. RediSearch TAG fields are\n * multi-valued per document, so this also matches documents carrying several of the tags.\n */\nfunction buildFilter(filter?: Record<string, unknown>): string {\n if (filter === undefined || Object.keys(filter).length === 0) {\n return '*';\n }\n const clauses = Object.entries(filter).map(([key, value]) => {\n const values = Array.isArray(value) ? value : [value];\n const alternation = values.map((entry) => escapeTag(String(entry))).join('|');\n return `@meta_${key}:{${alternation}}`;\n });\n return `(${clauses.join(' ')})`;\n}\n\n/**\n * A filter with an empty-array value can never match (nothing is a member of the empty set) — and\n * RediSearch has no valid empty-tag syntax, so callers short-circuit to an empty result instead of\n * emitting a broken query. This is the deny primitive (e.g. an actor with no capability tokens).\n */\nfunction filterMatchesNothing(filter?: Record<string, unknown>): boolean {\n if (filter === undefined) {\n return false;\n }\n return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);\n}\n\n/**\n * Reduce a raw user query to RediSearch query *terms*, the union of which becomes the text clause.\n *\n * This is the escaping story for the text leg, and it escapes by **construction** rather than by\n * quoting: a term is kept only as a run of letters, digits and `_`, so nothing that could carry\n * meaning in RediSearch's query language — `@ { } | ( ) * ~ - \" ' : ; , => %` and friends — can\n * survive into the emitted query. A query cannot therefore add a clause, negate one, open a field\n * selector, or widen the filter it is ANDed with. (Escaping each punctuation character instead, as\n * {@link escapeTag} does for TAG values, would be safe but *lossy*: RediSearch's own tokenizer split\n * the indexed text on that same punctuation, so an escaped `hello\\-world` term matches nothing.)\n *\n * Unicode letters and digits are kept — stripping to ASCII would silently make non-English corpora\n * unsearchable. Since `\\p{L}`/`\\p{N}` contain no RediSearch metacharacters, keeping them is safe.\n */\nfunction tokenizeQuery(query: string): string[] {\n return query.match(/[\\p{L}\\p{N}_]+/gu) ?? [];\n}\n\n/** Escape RediSearch TAG punctuation so an id/tenant value matches literally. */\nfunction escapeTag(value: string): string {\n return value.replace(/[^a-zA-Z0-9_]/g, (char) => `\\\\${char}`);\n}\n\n/** Escape `SCAN MATCH` glob metacharacters so a document id matches its chunk keys literally. */\nfunction escapeGlob(value: string): string {\n return value.replace(/[*?[\\]\\\\]/g, (char) => `\\\\${char}`);\n}\n\n/**\n * Parse a `SCAN` reply into `{ cursor, keys }`, tolerating both wire shapes: the RESP2 array\n * `[cursor, [key, …]]` and node-redis's RESP3 object `{ cursor, keys }`.\n */\nfunction parseScanReply(reply: unknown): { cursor: string; keys: string[] } {\n if (Array.isArray(reply) && reply.length >= 2) {\n const rawKeys = reply[1];\n return {\n cursor: toStr(reply[0]),\n keys: Array.isArray(rawKeys) ? rawKeys.map(toStr) : [],\n };\n }\n if (typeof reply === 'object' && reply !== null && 'cursor' in reply && 'keys' in reply) {\n const { cursor, keys } = reply as { cursor: unknown; keys: unknown };\n return {\n cursor: toStr(cursor),\n keys: Array.isArray(keys) ? keys.map(toStr) : [],\n };\n }\n return { cursor: '0', keys: [] };\n}\n\nfunction toStr(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);\n}\n\n/**\n * Which leg produced the reply, and therefore where a passage's `score` comes from:\n *\n * - `vector` — the KNN query. Score is the returned `vector_score` **distance**, turned into a\n * cosine similarity by `1 - distance`. Two array elements per result (`key`, `fields`).\n * - `lexical` — the `WITHSCORES` text query. The engine's BM25 relevance is already a\n * bigger-is-better score, and it arrives *inline*: three array elements per result\n * (`key`, `score`, `fields`), which is why the stride is not a constant.\n */\ntype ScoreSource = 'vector' | 'lexical';\n\n/**\n * Parse an `FT.SEARCH` reply into passages, tolerating both wire shapes: the RESP2 array\n * `[total, key, [f, v, …], …]` (ioredis / node-redis RESP2) and the RESP3 object\n * `{ results: [{ id, extra_attributes: { … } }] }` (node-redis's default). The `prefix` is stripped\n * back off the key to recover the chunk id; `scoreSource` selects the scoring convention.\n */\nfunction parseSearchReply(\n reply: unknown,\n prefix: string,\n scoreSource: ScoreSource = 'vector',\n): Passage[] {\n const stride = scoreSource === 'lexical' ? 3 : 2;\n if (Array.isArray(reply)) {\n const passages: Passage[] = [];\n for (let index = 1; index + stride - 1 < reply.length; index += stride) {\n const rawFields = reply[index + stride - 1];\n if (Array.isArray(rawFields)) {\n const attrs: Record<string, string> = {};\n for (let field = 0; field + 1 < rawFields.length; field += 2) {\n attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);\n }\n const inlineScore = scoreSource === 'lexical' ? toStr(reply[index + 1]) : undefined;\n passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));\n }\n }\n return passages;\n }\n if (typeof reply === 'object' && reply !== null && 'results' in reply) {\n const results = (reply as { results: unknown }).results;\n if (Array.isArray(results)) {\n return results.map((result) =>\n toPassage(\n readId(result),\n readAttributes(result),\n prefix,\n scoreSource,\n scoreSource === 'lexical' ? readScore(result) : undefined,\n ),\n );\n }\n }\n return [];\n}\n\n/** RESP3 `WITHSCORES` puts the relevance score on the result object rather than inline in the array. */\nfunction readScore(result: unknown): string | undefined {\n if (typeof result === 'object' && result !== null && 'score' in result) {\n return toStr((result as { score: unknown }).score);\n }\n return undefined;\n}\n\nfunction readId(result: unknown): string {\n if (typeof result === 'object' && result !== null && 'id' in result) {\n return toStr((result as { id: unknown }).id);\n }\n return '';\n}\n\nfunction readAttributes(result: unknown): Record<string, string> {\n const attrs: Record<string, string> = {};\n if (typeof result === 'object' && result !== null && 'extra_attributes' in result) {\n const raw = (result as { extra_attributes: unknown }).extra_attributes;\n if (typeof raw === 'object' && raw !== null) {\n for (const [key, value] of Object.entries(raw)) {\n attrs[key] = toStr(value);\n }\n }\n }\n return attrs;\n}\n\nfunction toPassage(\n rawId: string,\n attrs: Record<string, string>,\n prefix: string,\n scoreSource: ScoreSource = 'vector',\n inlineScore?: string,\n): Passage {\n const score =\n scoreSource === 'lexical' ? Number(inlineScore ?? '0') : 1 - Number(attrs.vector_score ?? '1');\n const source = attrs.source;\n return {\n id: rawId.startsWith(prefix) ? rawId.slice(prefix.length) : rawId,\n text: attrs.text ?? '',\n score: Number.isFinite(score) ? score : 0,\n ...(source !== undefined ? { source } : {}),\n ...parseMetadata(attrs.metadata_json),\n };\n}\n\nfunction parseMetadata(raw: string | undefined): { metadata?: Record<string, unknown> } {\n if (raw === undefined) {\n return {};\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed === 'object' && parsed !== null) {\n return { metadata: parsed as Record<string, unknown> };\n }\n } catch {\n // not JSON — no metadata\n }\n return {};\n}\n","import type {\n EmbeddingProvider,\n Passage,\n RetrieveOptions,\n Retriever,\n} from '@dudousxd/nestjs-agent-core';\nimport type { VectorStore } from './vector-store.js';\n\n/**\n * Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:\n * embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool\n * via `createRetrievalTool`, or into `AgentModule.forRoot({ retrieval })` for inject mode).\n */\nexport class EmbeddingRetriever implements Retriever {\n constructor(\n private readonly embedder: EmbeddingProvider,\n private readonly store: VectorStore,\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const [embedding] = await this.embedder.embed([query]);\n if (embedding === undefined) {\n return [];\n }\n return this.store.search(embedding, {\n topK: options.topK ?? 5,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport { matchesFilter } from './filter.js';\nimport type { ChunkRecord } from './ingest.js';\nimport { documentIdOf } from './vector-store.js';\n\nexport interface KeywordRetrieverOptions {\n /** BM25 term-frequency saturation. Default 1.5. */\n k1?: number;\n /** BM25 length normalization, 0 (off) … 1 (full). Default 0.75. */\n b?: number;\n}\n\ninterface IndexedDoc {\n record: ChunkRecord;\n /** term → frequency within this doc. */\n frequencies: Map<string, number>;\n length: number;\n}\n\n/**\n * An in-memory BM25 lexical retriever — the keyword half of hybrid search. Feed it the SAME\n * {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids\n * line up and `HybridRetriever` can fuse the two rankings. Full BM25: idf, term-frequency saturation\n * (`k1`), and document-length normalization (`b`).\n *\n * Because it holds its OWN copy of each chunk's text, it needs the same delete-sync as the vector\n * store: whenever you call {@link import('./vector-store.js').VectorStore.remove}, call\n * {@link KeywordRetriever.remove} with the same document id. Skipping it leaves the document\n * lexically retrievable — `retrieve` would keep returning the removed passage's full text.\n */\nexport class KeywordRetriever implements Retriever {\n private readonly docs = new Map<string, IndexedDoc>();\n private readonly documentFrequency = new Map<string, number>();\n private totalLength = 0;\n private readonly k1: number;\n private readonly b: number;\n\n constructor(options: KeywordRetrieverOptions = {}) {\n this.k1 = options.k1 ?? 1.5;\n this.b = options.b ?? 0.75;\n }\n\n /** How many chunks are currently indexed. */\n get size(): number {\n return this.docs.size;\n }\n\n /** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */\n add(records: ChunkRecord[]): void {\n for (const record of records) {\n this.removeChunk(record.id);\n const tokens = tokenize(record.text);\n const frequencies = new Map<string, number>();\n for (const token of tokens) {\n frequencies.set(token, (frequencies.get(token) ?? 0) + 1);\n }\n for (const term of frequencies.keys()) {\n this.documentFrequency.set(term, (this.documentFrequency.get(term) ?? 0) + 1);\n }\n this.docs.set(record.id, { record, frequencies, length: tokens.length });\n this.totalLength += tokens.length;\n }\n }\n\n /**\n * Forget a source document — every chunk whose id is `${documentId}` or `${documentId}#<n>`, the\n * same id scheme (and the same {@link documentIdOf} collapse) the vector stores use for their\n * `remove`. The keyword half of a hybrid index keeps its own copy of the chunk text, so a document\n * dropped from the vector store stays fully retrievable here until this is called. Unknown ids are\n * a no-op.\n */\n remove(documentId: string): void {\n for (const id of [...this.docs.keys()]) {\n if (id === documentId || documentIdOf(id) === documentId) {\n this.removeChunk(id);\n }\n }\n }\n\n /** Drop every indexed chunk, resetting the corpus statistics with it. */\n clear(): void {\n this.docs.clear();\n this.documentFrequency.clear();\n this.totalLength = 0;\n }\n\n /** Drop ONE chunk by its exact id, decrementing the corpus statistics it contributed. */\n private removeChunk(id: string): void {\n const existing = this.docs.get(id);\n if (existing === undefined) {\n return;\n }\n for (const term of existing.frequencies.keys()) {\n const next = (this.documentFrequency.get(term) ?? 1) - 1;\n if (next <= 0) {\n this.documentFrequency.delete(term);\n } else {\n this.documentFrequency.set(term, next);\n }\n }\n this.totalLength -= existing.length;\n this.docs.delete(id);\n }\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const total = this.docs.size;\n if (total === 0) {\n return [];\n }\n const averageLength = this.totalLength / total;\n const queryTerms = new Set(tokenize(query));\n const scored: Passage[] = [];\n for (const { record, frequencies, length } of this.docs.values()) {\n if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {\n continue;\n }\n let score = 0;\n for (const term of queryTerms) {\n const termFrequency = frequencies.get(term);\n if (termFrequency === undefined) {\n continue;\n }\n const df = this.documentFrequency.get(term) ?? 0;\n const idf = Math.log(1 + (total - df + 0.5) / (df + 0.5));\n const denominator =\n termFrequency + this.k1 * (1 - this.b + (this.b * length) / averageLength);\n score += idf * ((termFrequency * (this.k1 + 1)) / denominator);\n }\n if (score > 0) {\n scored.push({\n id: record.id,\n text: record.text,\n score,\n ...(record.source !== undefined ? { source: record.source } : {}),\n ...(record.metadata !== undefined ? { metadata: record.metadata } : {}),\n });\n }\n }\n return scored.sort((a, b) => b.score - a.score).slice(0, options.topK ?? 5);\n }\n}\n\nfunction tokenize(text: string): string[] {\n return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\nimport type { LexicalVectorStore } from './vector-store.js';\n\n/**\n * The keyword half of hybrid search, served by the store's own full-text index — the mirror of\n * {@link import('./embedding-retriever.js').EmbeddingRetriever}, which serves the dense half. It\n * holds no state: every query goes to the store, so any process that can reach the store can run\n * lexical search, whether or not it did the ingestion.\n *\n * Compose it exactly like the dense leg:\n *\n * ```ts\n * const retriever = new HybridRetriever([\n * new EmbeddingRetriever(embedder, store),\n * new LexicalRetriever(store),\n * ]);\n * ```\n *\n * Both legs read the same chunks from the same index, so their ids line up by construction and\n * `HybridRetriever`'s RRF fuses them without either side needing a score on a common scale.\n *\n * Prefer this over {@link import('./keyword-retriever.js').KeywordRetriever} whenever the store\n * supports it (see {@link import('./vector-store.js').isLexicalVectorStore}) and ingestion and\n * search can run in different processes; `KeywordRetriever` remains the answer for `MemoryVectorStore`\n * and single-process deployments.\n */\nexport class LexicalRetriever implements Retriever {\n constructor(private readonly store: LexicalVectorStore) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return this.store.searchText(query, {\n topK: options.topK ?? 5,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\n\nexport interface HybridRetrieverOptions {\n /** RRF constant — larger flattens the rank weighting. Default 60 (the value from the original paper). */\n k?: number;\n /** Candidates pulled from each retriever before fusion. Default 20. */\n fetchTopK?: number;\n /** Per-retriever multipliers on each RRF contribution (same order as `retrievers`). Default all 1. */\n weights?: number[];\n}\n\n/**\n * Fuses several retrievers with Reciprocal Rank Fusion — the robust way to combine, say, dense\n * vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the\n * lists it appears in, so it needs no score normalization between incompatible scales (cosine vs\n * BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.\n */\nexport class HybridRetriever implements Retriever {\n constructor(\n private readonly retrievers: Retriever[],\n private readonly options: HybridRetrieverOptions = {},\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const k = this.options.k ?? 60;\n const fetchTopK = this.options.fetchTopK ?? 20;\n const weights = this.options.weights;\n const lists = await Promise.all(\n this.retrievers.map((retriever) =>\n retriever.retrieve(query, {\n topK: fetchTopK,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n }),\n ),\n );\n\n const fused = new Map<string, { passage: Passage; score: number }>();\n lists.forEach((list, listIndex) => {\n const weight = weights?.[listIndex] ?? 1;\n list.forEach((passage, rank) => {\n const contribution = weight / (k + rank + 1);\n const existing = fused.get(passage.id);\n if (existing !== undefined) {\n existing.score += contribution;\n } else {\n fused.set(passage.id, { passage, score: contribution });\n }\n });\n });\n\n return [...fused.values()]\n .sort((a, b) => b.score - a.score)\n .slice(0, options.topK ?? 5)\n .map((entry) => ({ ...entry.passage, score: entry.score }));\n }\n}\n","import type { Passage, Reranker, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\n\nexport interface RerankingRetrieverOptions {\n /** How many candidates to pull from the base retriever before reranking. Default 20. */\n fetchTopK?: number;\n}\n\n/**\n * Two-stage retrieval: over-fetch cheap candidates from a base `Retriever`, then reorder them with a\n * stronger {@link Reranker} and keep the top few. The standard precision boost — a fast first stage\n * casts a wide net, a slow accurate second stage sharpens it. Composes over ANY retriever (vector,\n * hybrid, keyword), and is itself a `Retriever`.\n */\nexport class RerankingRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly reranker: Reranker,\n private readonly options: RerankingRetrieverOptions = {},\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n const candidates = await this.base.retrieve(query, {\n topK: this.options.fetchTopK ?? 20,\n ...(options.filter !== undefined ? { filter: options.filter } : {}),\n });\n return this.reranker.rerank(query, candidates, { topK: options.topK ?? 5 });\n }\n}\n","import type { Passage, RetrieveOptions, Retriever } from '@dudousxd/nestjs-agent-core';\n\n/**\n * Wraps a `Retriever` so every `retrieve` call ANDs a fixed metadata `filter` on top of whatever the\n * caller passes — the owner/tenant-scoping primitive. `new FilteredRetriever(base, { ownerId })` only\n * ever returns that owner's passages, so a shared vector store serves per-user / per-tenant RAG\n * without the agent (or a crafted query) reaching across the boundary.\n *\n * The fixed filter takes precedence on key conflicts: a caller can narrow further but cannot widen\n * past the scope. Composes over ANY retriever (vector, hybrid, reranked) and is itself a `Retriever`.\n */\nexport class FilteredRetriever implements Retriever {\n constructor(\n private readonly base: Retriever,\n private readonly filter: Record<string, unknown>,\n ) {}\n\n async retrieve(query: string, options: RetrieveOptions = {}): Promise<Passage[]> {\n return this.base.retrieve(query, {\n ...options,\n filter: { ...options.filter, ...this.filter },\n });\n }\n}\n","import type { EmbeddingProvider } from '@dudousxd/nestjs-agent-core';\nimport { type ChunkOptions, chunkText } from './chunk.js';\nimport type { VectorRecord, VectorStore } from './vector-store.js';\n\n/** A source document to ingest. `id` scopes the chunk ids (`${id}#${n}`); `source` is the citation. */\nexport interface IngestDocument {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\n/** A chunked, not-yet-embedded record. Feed the same array to a vector store AND a keyword index so\n * both retrievers key on identical chunk ids (which is what lets hybrid fusion line them up). */\nexport interface ChunkRecord {\n id: string;\n text: string;\n source?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface IngestChunksOptions {\n embedder: EmbeddingProvider;\n store: VectorStore;\n}\n\nexport interface IngestOptions extends ChunkOptions, IngestChunksOptions {}\n\n/**\n * Split documents into chunk records. Chunk ids are `${doc.id}#${index}`, so re-chunking the same\n * document produces the same ids (upsert overwrites in place rather than duplicating).\n */\nexport function chunkDocuments(\n documents: IngestDocument[],\n options: ChunkOptions = {},\n): ChunkRecord[] {\n const chunks: ChunkRecord[] = [];\n for (const document of documents) {\n chunkText(document.text, options).forEach((text, index) => {\n chunks.push({\n id: `${document.id}#${index}`,\n text,\n ...(document.source !== undefined ? { source: document.source } : {}),\n ...(document.metadata !== undefined ? { metadata: document.metadata } : {}),\n });\n });\n }\n return chunks;\n}\n\n/** Embed pre-chunked records (one batched `embed`) and upsert them. Returns the record count. */\nexport async function ingestChunks(\n chunks: ChunkRecord[],\n options: IngestChunksOptions,\n): Promise<number> {\n if (chunks.length === 0) {\n return 0;\n }\n const embeddings = await options.embedder.embed(chunks.map((chunk) => chunk.text));\n const records: VectorRecord[] = chunks.map((chunk, index) => ({\n ...chunk,\n embedding: embeddings[index] ?? [],\n }));\n await options.store.upsert(records);\n return records.length;\n}\n\n/** Chunk → embed → upsert in one call. For hybrid search, use {@link chunkDocuments} +\n * {@link ingestChunks} and feed the same chunks to a {@link import('./keyword-retriever.js').KeywordRetriever}. */\nexport async function ingestDocuments(\n documents: IngestDocument[],\n options: IngestOptions,\n): Promise<number> {\n return ingestChunks(chunkDocuments(documents, options), options);\n}\n","import type { Passage, Retriever, ToolHandler, ToolSpec } from '@dudousxd/nestjs-agent-core';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\n\nexport interface RetrievalToolOptions {\n /** Tool name the model sees. Default `search_knowledge`. */\n name?: string;\n description?: string;\n /** Passages per search. Default lets the retriever decide (typically 5). */\n topK?: number;\n}\n\n/** A functional tool (`{ spec, handler }`) — pass it to nestjs `provideAgentTool`. */\nexport interface RetrievalTool {\n spec: ToolSpec;\n handler: ToolHandler;\n}\n\nfunction isQueryInput(value: unknown): value is { query: string } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'query' in value &&\n typeof (value as Record<'query', unknown>).query === 'string'\n );\n}\n\n/** A hand-rolled Standard Schema for `{ query: string }` — keeps this package validation-lib-free. */\nconst querySchema: StandardSchemaV1<{ query: string }> = {\n '~standard': {\n version: 1,\n vendor: 'nestjs-agent-rag',\n validate(value: unknown) {\n return isQueryInput(value)\n ? { value: { query: value.query } }\n : { issues: [{ message: 'retrieval tool expects { query: string }' }] };\n },\n },\n};\n\n/**\n * Builds the agentic-retrieval tool: a `read`-kind `search_knowledge(query)` the model calls when it\n * decides it needs context. Its output `{ passages }` persists as the tool call's result, so sources\n * flow to the frontend and telescope through the normal tool-call machinery (the citation surface).\n *\n * ```ts\n * providers: [provideAgentTool(createRetrievalTool(retriever))]\n * ```\n */\nexport function createRetrievalTool(\n retriever: Retriever,\n options: RetrievalToolOptions = {},\n): RetrievalTool {\n const topK = options.topK;\n return {\n spec: {\n name: options.name ?? 'search_knowledge',\n kind: 'read',\n description:\n options.description ??\n 'Search the knowledge base for passages relevant to a query. Returns passages with their source for citation.',\n inputSchema: querySchema,\n },\n handler: {\n async execute(input: unknown): Promise<{ passages: Passage[] }> {\n if (!isQueryInput(input)) {\n throw new Error('retrieval tool expects { query: string }');\n }\n const passages = await retriever.retrieve(input.query, topK !== undefined ? { topK } : {});\n return { passages };\n },\n },\n };\n}\n"],"mappings":"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","names":["chunkText","text","options","chunkSize","overlap","normalized","trim","length","chunks","start","end","Math","min","breakAt","window","slice","boundary","max","lastIndexOf","chunk","push","isLexicalVectorStore","store","searchText","documentIdOf","chunkId","replace","matchesFilter","metadata","filter","entries","Object","undefined","length","every","key","expected","Array","isArray","actual","some","value","includes","MemoryVectorStore","records","Map","upsert","record","set","id","remove","documentId","keys","documentIdOf","delete","listDocuments","filter","documents","values","undefined","matchesFilter","metadata","has","search","embedding","options","scored","push","text","score","cosineSimilarity","source","sort","a","b","slice","topK","dot","normA","normB","length","Math","min","index","valueA","valueB","denominator","sqrt","PgVectorStore","table","dimensions","client","options","ensureSchema","query","upsert","records","record","id","text","source","metadata","undefined","JSON","stringify","toVectorLiteral","embedding","remove","documentId","DOCUMENT_ID_FROM_CHUNK","listDocuments","filter","where","buildWhere","rows","sql","params","map","row","doc_id","search","vector","topK","score","Number","startIndex","Object","keys","length","clauses","scalar","index","key","value","entries","Array","isArray","push","keyParam","arrParam","String","join","RedisVectorSchemaMismatchError","Error","index","field","expected","actual","remedy","name","RedisVectorStore","prefix","dimensions","filterableFields","lexicalScorer","client","options","ensureSchema","info","sendCommand","createIndex","reconcileSchema","attributes","parseIndexAttributes","size","vector","get","undefined","missing","existing","push","type","schema","String","upsert","records","record","args","id","text","encodeVector","embedding","source","metadata","JSON","stringify","value","tag","Array","isArray","map","join","remove","documentId","keys","Set","pattern","escapeGlob","cursor","reply","page","parseScanReply","key","add","listDocuments","filter","filterMatchesNothing","query","buildFilter","documents","Map","batchSize","offset","passages","parseSearchReply","passage","documentIdOf","has","set","length","values","search","topK","searchText","terms","tokenizeQuery","filterQuery","textQuery","entry","readInfoField","fields","readKeyValues","attribute","identifier","dim","Number","toUpperCase","isFinite","toStr","toLowerCase","Object","entries","Buffer","from","Float32Array","buffer","clauses","alternation","escapeTag","some","match","replace","char","rawKeys","isBuffer","toString","scoreSource","stride","rawFields","attrs","inlineScore","toPassage","results","result","readId","readAttributes","readScore","score","raw","extra_attributes","rawId","vector_score","startsWith","slice","parseMetadata","metadata_json","parsed","parse","EmbeddingRetriever","embedder","store","retrieve","query","options","embedding","embed","undefined","search","topK","filter","KeywordRetriever","docs","Map","documentFrequency","totalLength","k1","b","options","size","add","records","record","removeChunk","id","tokens","tokenize","text","frequencies","token","set","get","term","keys","length","remove","documentId","documentIdOf","clear","existing","undefined","next","delete","retrieve","query","total","averageLength","queryTerms","Set","scored","values","filter","matchesFilter","metadata","score","termFrequency","df","idf","Math","log","denominator","push","source","sort","a","slice","topK","toLowerCase","match","LexicalRetriever","store","retrieve","query","options","searchText","topK","filter","undefined","HybridRetriever","retrievers","options","retrieve","query","k","fetchTopK","weights","lists","Promise","all","map","retriever","topK","filter","undefined","fused","Map","forEach","list","listIndex","weight","passage","rank","contribution","existing","get","id","score","set","values","sort","a","b","slice","entry","RerankingRetriever","base","reranker","options","retrieve","query","candidates","topK","fetchTopK","filter","undefined","rerank","FilteredRetriever","base","filter","retrieve","query","options","chunkDocuments","documents","options","chunks","document","chunkText","text","forEach","index","push","id","source","undefined","metadata","ingestChunks","length","embeddings","embedder","embed","map","chunk","records","embedding","store","upsert","ingestDocuments","isQueryInput","value","query","querySchema","version","vendor","validate","issues","message","createRetrievalTool","retriever","options","topK","spec","name","kind","description","inputSchema","handler","execute","input","Error","passages","retrieve","undefined"]}
|