{"version":3,"file":"rag.mjs","names":["chunkText","runRetrieve"],"sources":["../../../../../../../ai/src/rag/rag.ts"],"sourcesContent":["import { resolveDefaultStore } from \"../config\";\nimport { chunk as chunkText } from \"./chunk/chunk\";\nimport type { ChunkOptions } from \"./contracts/chunk-options.type\";\nimport type { RetrieveOptions, RetrieveResult } from \"./contracts/citation.type\";\nimport type {\n  Rag,\n  RagAsToolOptions,\n  RagConfig,\n} from \"./contracts/rag-config.type\";\nimport type { RagDocument } from \"./contracts/rag-document.type\";\nimport { ragAsTool } from \"./as-tool\";\nimport { retrieve as runRetrieve, type StoredChunk } from \"./retrieve\";\nimport { cacheVectorStore } from \"./store/cache-vector-store\";\nimport type { VectorStore } from \"./store/vector-store.contract\";\n\nconst DEFAULT_NAME = \"rag\";\nconst DEFAULT_NAMESPACE_PREFIX = \"ai.rag\";\n\n/**\n * Max chunk texts embedded per `embedder.embedMany()` call. One call is\n * one provider request, so a giant document is sub-batched to stay under\n * the provider's per-request token cap (the design's \"chunk larger than\n * provider per-request cap\" guard).\n */\nconst DEFAULT_MAX_BATCH = 96;\n\n/**\n * Create a RAG pipeline: **chunk → embed → vector store → retrieve →\n * rerank → cite**, reusing the app's `ai.embedder` for embedding, a\n * `@warlock.js/cache` `CacheDriver` as the vector store, and the\n * composite-as-tool engine to expose retrieval as a tool.\n *\n * Resolution is loud at construction (mirroring `memory()`):\n * - `embedder` is required — a provider with no embedder must be caught\n *   here, not at first index.\n * - `store` falls back to `ai.config({ defaultStore })`; if neither\n *   resolves, construction throws.\n *\n * `retrieve()` is return-only — it never auto-injects into a prompt; the\n * caller formats the cited chunks (or uses `asTool()` for the agent loop).\n * The reranker is OFF by default (cosine-only) unless `config.reranker`\n * is set.\n *\n * @example\n * import { ai } from \"@warlock.js/ai\";\n * import { MemoryCacheDriver } from \"@warlock.js/cache\";\n *\n * const kb = ai.rag({\n *   name: \"docs\",\n *   embedder: openai.embedder({ name: \"text-embedding-3-small\" }),\n *   store: new MemoryCacheDriver(),\n *   chunk: { type: \"markdown\", size: 800, overlap: 120 },\n * });\n *\n * await kb.index([{ id: \"guide\", text: longMarkdown, metadata: { url: \"/guide\" } }]);\n * const { chunks } = await kb.retrieve(\"how do I configure caching?\", { topK: 4 });\n */\nexport function rag(config: RagConfig): Rag {\n  const name = config.name ?? DEFAULT_NAME;\n\n  if (!config.embedder) {\n    throw new Error(\n      `rag(\"${name}\"): an \\`embedder\\` is required — pass one from a provider that supports embeddings (e.g. openai.embedder({ name: \"text-embedding-3-small\" }))`,\n    );\n  }\n\n  const driver = config.store ?? resolveDefaultStore();\n\n  if (!driver) {\n    throw new Error(\n      `rag(\"${name}\"): no store — pass \\`store\\` (a vector-capable @warlock.js/cache CacheDriver) or call \\`ai.config({ defaultStore })\\` at app boot before constructing the rag`,\n    );\n  }\n\n  const store: VectorStore = cacheVectorStore(driver);\n  const namespace = config.namespace ?? `${DEFAULT_NAMESPACE_PREFIX}.${name}`;\n  const embedder = config.embedder;\n\n  // Captured at first index for the dimension-mismatch guard in retrieve().\n  let indexedDimensions: number | undefined;\n\n  const instance: Rag = {\n    name,\n\n    async index(\n      docs: RagDocument[],\n      chunkOverride?: ChunkOptions,\n    ): Promise<{ chunks: number }> {\n      const chunkOptions = chunkOverride ?? config.chunk;\n\n      // Ingestion guardrails (D5) — fail BEFORE any embedding spend.\n      const limits = config.limits;\n      if (limits?.maxDocuments !== undefined && docs.length > limits.maxDocuments) {\n        throw new Error(\n          `rag(\"${name}\"): index() received ${docs.length} documents, exceeding the configured maxDocuments of ${limits.maxDocuments}`,\n        );\n      }\n      if (limits?.maxBytes !== undefined) {\n        const totalBytes = docs.reduce(\n          (sum, doc) => sum + Buffer.byteLength(doc.text ?? \"\"),\n          0,\n        );\n        if (totalBytes > limits.maxBytes) {\n          throw new Error(\n            `rag(\"${name}\"): index() received ${totalBytes} bytes of document text, exceeding the configured maxBytes of ${limits.maxBytes}`,\n          );\n        }\n      }\n\n      // Flatten every document into stored-chunk records + their texts,\n      // preserving document order so a single batched embed maps back 1:1.\n      const records: { key: string; value: StoredChunk; text: string; tags?: string[] }[] = [];\n\n      for (const doc of docs) {\n        const pieces = chunkText(doc.text, chunkOptions);\n\n        for (const piece of pieces) {\n          const value: StoredChunk = {\n            sourceId: doc.id,\n            chunkIndex: piece.index,\n            span: piece.span,\n            text: piece.text,\n            metadata: doc.metadata,\n          };\n\n          records.push({\n            key: keyFor(namespace, doc.id, piece.index),\n            value,\n            text: piece.text,\n            tags: doc.tags,\n          });\n        }\n      }\n\n      // Empty / whitespace-only documents yield zero chunks — write\n      // nothing and never embed an empty batch.\n      if (records.length === 0) {\n        return { chunks: 0 };\n      }\n\n      // Chunk cap (D5) — checked after chunking, still before embedding.\n      if (limits?.maxChunks !== undefined && records.length > limits.maxChunks) {\n        throw new Error(\n          `rag(\"${name}\"): index() produced ${records.length} chunks, exceeding the configured maxChunks of ${limits.maxChunks}`,\n        );\n      }\n\n      // Sub-batch the embed calls so one giant document does not blow the\n      // provider's per-request token cap.\n      for (let offset = 0; offset < records.length; offset += DEFAULT_MAX_BATCH) {\n        const batch = records.slice(offset, offset + DEFAULT_MAX_BATCH);\n        const { vectors, dimensions } = await embedder.embedMany(\n          batch.map((record) => record.text),\n        );\n\n        if (indexedDimensions === undefined && dimensions !== 0) {\n          indexedDimensions = dimensions;\n        }\n\n        await Promise.all(\n          batch.map((record, position) =>\n            store.upsert(record.key, record.value, vectors[position], record.tags),\n          ),\n        );\n      }\n\n      return { chunks: records.length };\n    },\n\n    async retrieve(query: string, options?: RetrieveOptions): Promise<RetrieveResult> {\n      return runRetrieve(\n        query,\n        {\n          embedder,\n          store,\n          namespace,\n          reranker: config.reranker,\n          defaults: config.retrieve,\n          indexedDimensions,\n        },\n        options,\n      );\n    },\n\n    async clear(): Promise<void> {\n      await store.removeNamespace(namespace);\n    },\n\n    asTool(options?: RagAsToolOptions) {\n      return ragAsTool(name, (query, retrieveOptions) => instance.retrieve(query, retrieveOptions), options);\n    },\n  };\n\n  return instance;\n}\n\n/**\n * Namespaced key for a stored chunk. Uses the `.` separator (matching\n * `SemanticMemory.keyFor`) so namespace-prefix filtering on the returned\n * `hit.key` stays aligned with the cache's `parseKey` normalization.\n */\nfunction keyFor(namespace: string, sourceId: string, chunkIndex: number): string {\n  return `${namespace}.${sourceId}.${chunkIndex}`;\n}\n"],"mappings":";;;;;;;AAeA,MAAM,eAAe;AACrB,MAAM,2BAA2B;;;;;;;AAQjC,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiC1B,SAAgB,IAAI,QAAwB;CAC1C,MAAM,OAAO,OAAO,QAAQ;CAE5B,IAAI,CAAC,OAAO,UACV,MAAM,IAAI,MACR,QAAQ,KAAK,+IACf;CAGF,MAAM,SAAS,OAAO,SAAS,oBAAoB;CAEnD,IAAI,CAAC,QACH,MAAM,IAAI,MACR,QAAQ,KAAK,+JACf;CAGF,MAAM,QAAqB,iBAAiB,MAAM;CAClD,MAAM,YAAY,OAAO,aAAa,GAAG,yBAAyB,GAAG;CACrE,MAAM,WAAW,OAAO;CAGxB,IAAI;CAEJ,MAAM,WAAgB;EACpB;EAEA,MAAM,MACJ,MACA,eAC6B;GAC7B,MAAM,eAAe,iBAAiB,OAAO;GAG7C,MAAM,SAAS,OAAO;GACtB,IAAI,QAAQ,iBAAiB,UAAa,KAAK,SAAS,OAAO,cAC7D,MAAM,IAAI,MACR,QAAQ,KAAK,uBAAuB,KAAK,OAAO,uDAAuD,OAAO,cAChH;GAEF,IAAI,QAAQ,aAAa,QAAW;IAClC,MAAM,aAAa,KAAK,QACrB,KAAK,QAAQ,MAAM,OAAO,WAAW,IAAI,QAAQ,EAAE,GACpD,CACF;IACA,IAAI,aAAa,OAAO,UACtB,MAAM,IAAI,MACR,QAAQ,KAAK,uBAAuB,WAAW,gEAAgE,OAAO,UACxH;GAEJ;GAIA,MAAM,UAAgF,CAAC;GAEvF,KAAK,MAAM,OAAO,MAAM;IACtB,MAAM,SAASA,MAAU,IAAI,MAAM,YAAY;IAE/C,KAAK,MAAM,SAAS,QAAQ;KAC1B,MAAM,QAAqB;MACzB,UAAU,IAAI;MACd,YAAY,MAAM;MAClB,MAAM,MAAM;MACZ,MAAM,MAAM;MACZ,UAAU,IAAI;KAChB;KAEA,QAAQ,KAAK;MACX,KAAK,OAAO,WAAW,IAAI,IAAI,MAAM,KAAK;MAC1C;MACA,MAAM,MAAM;MACZ,MAAM,IAAI;KACZ,CAAC;IACH;GACF;GAIA,IAAI,QAAQ,WAAW,GACrB,OAAO,EAAE,QAAQ,EAAE;GAIrB,IAAI,QAAQ,cAAc,UAAa,QAAQ,SAAS,OAAO,WAC7D,MAAM,IAAI,MACR,QAAQ,KAAK,uBAAuB,QAAQ,OAAO,iDAAiD,OAAO,WAC7G;GAKF,KAAK,IAAI,SAAS,GAAG,SAAS,QAAQ,QAAQ,UAAU,mBAAmB;IACzE,MAAM,QAAQ,QAAQ,MAAM,QAAQ,SAAS,iBAAiB;IAC9D,MAAM,EAAE,SAAS,eAAe,MAAM,SAAS,UAC7C,MAAM,KAAK,WAAW,OAAO,IAAI,CACnC;IAEA,IAAI,sBAAsB,UAAa,eAAe,GACpD,oBAAoB;IAGtB,MAAM,QAAQ,IACZ,MAAM,KAAK,QAAQ,aACjB,MAAM,OAAO,OAAO,KAAK,OAAO,OAAO,QAAQ,WAAW,OAAO,IAAI,CACvE,CACF;GACF;GAEA,OAAO,EAAE,QAAQ,QAAQ,OAAO;EAClC;EAEA,MAAM,SAAS,OAAe,SAAoD;GAChF,OAAOC,SACL,OACA;IACE;IACA;IACA;IACA,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB;GACF,GACA,OACF;EACF;EAEA,MAAM,QAAuB;GAC3B,MAAM,MAAM,gBAAgB,SAAS;EACvC;EAEA,OAAO,SAA4B;GACjC,OAAO,UAAU,OAAO,OAAO,oBAAoB,SAAS,SAAS,OAAO,eAAe,GAAG,OAAO;EACvG;CACF;CAEA,OAAO;AACT;;;;;;AAOA,SAAS,OAAO,WAAmB,UAAkB,YAA4B;CAC/E,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG;AACrC"}