@devilsdev/rag-pipeline-utils 2.4.5 → 2.6.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.
Files changed (258) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +431 -0
  3. package/README.md +418 -117
  4. package/SECURITY.md +5 -5
  5. package/dist/experimental.cjs +2 -0
  6. package/dist/experimental.d.ts +1 -0
  7. package/dist/experimental.mjs +5 -0
  8. package/dist/index.cjs +6442 -1355
  9. package/dist/index.d.ts +129 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/testing.cjs +2 -0
  12. package/dist/testing.d.ts +1 -0
  13. package/dist/testing.mjs +5 -0
  14. package/dist/types/adapters/ai-sdk.d.ts +53 -0
  15. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  16. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  17. package/dist/types/agentic/query-planner.d.ts +145 -0
  18. package/dist/types/agentic/self-critique.d.ts +45 -0
  19. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  20. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  21. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  22. package/dist/types/ai/multimodal.d.ts +39 -0
  23. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  24. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  25. package/dist/types/cache/component-keys.d.ts +7 -0
  26. package/dist/types/cache/memory-cache.d.ts +54 -0
  27. package/dist/types/cache/pipeline-cache.d.ts +66 -0
  28. package/dist/types/cache/redis-cache.d.ts +46 -0
  29. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  30. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  31. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  32. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  33. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  34. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  35. package/dist/types/citation/citation-tracker.d.ts +64 -0
  36. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  37. package/dist/types/citation/source-mapper.d.ts +37 -0
  38. package/dist/types/connectors/anthropic-connector.d.ts +97 -0
  39. package/dist/types/connectors/base-connector.d.ts +87 -0
  40. package/dist/types/connectors/cohere-connector.d.ts +94 -0
  41. package/dist/types/connectors/connector-utils.d.ts +110 -0
  42. package/dist/types/connectors/local-embedder.d.ts +95 -0
  43. package/dist/types/connectors/memory-retriever.d.ts +99 -0
  44. package/dist/types/connectors/ollama-connector.d.ts +115 -0
  45. package/dist/types/connectors/openai-connector.d.ts +129 -0
  46. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  47. package/dist/types/core/create-pipeline.d.ts +8 -0
  48. package/dist/types/core/plugin-registry.d.ts +136 -0
  49. package/dist/types/core/stage-contracts.d.ts +36 -0
  50. package/dist/types/core/validate-plugin.d.ts +45 -0
  51. package/dist/types/cost/cost-calculator.d.ts +152 -0
  52. package/dist/types/cost/cost-tracker.d.ts +126 -0
  53. package/dist/types/cost/token-budget.d.ts +110 -0
  54. package/dist/types/dag/core/error-context.d.ts +239 -0
  55. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  56. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  57. package/dist/types/dag/dag-engine.d.ts +156 -0
  58. package/dist/types/dag/dag-node.d.ts +37 -0
  59. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  60. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  61. package/dist/types/debug/execution-tracer.d.ts +57 -0
  62. package/dist/types/debug/trace-inspector.d.ts +31 -0
  63. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  64. package/dist/types/enterprise/data-governance.d.ts +203 -0
  65. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  66. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  67. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  68. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  69. package/dist/types/evaluate/groundedness.d.ts +60 -0
  70. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  71. package/dist/types/evaluate/relevance.d.ts +21 -0
  72. package/dist/types/evaluate/scoring.d.ts +123 -0
  73. package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
  74. package/dist/types/experimental.d.ts +15 -0
  75. package/dist/types/graph/entity-extractor.d.ts +84 -0
  76. package/dist/types/graph/graph-index.d.ts +49 -0
  77. package/dist/types/graph/graph-retriever.d.ts +81 -0
  78. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  79. package/dist/types/grounding/check-answer.d.ts +101 -0
  80. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  81. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  82. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  83. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  84. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  85. package/dist/types/mcp/mcp-server.d.ts +85 -0
  86. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  87. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  88. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  89. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  90. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  91. package/dist/types/retrieval/file-vector-store.d.ts +99 -0
  92. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  93. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  94. package/dist/types/security/jwt-config.d.ts +80 -0
  95. package/dist/types/security/jwt-validator.d.ts +195 -0
  96. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  97. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  98. package/dist/types/security/validator.d.ts +5 -0
  99. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  100. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  101. package/dist/types/streaming/stream-router.d.ts +88 -0
  102. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  103. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  104. package/dist/types/templates/scaffold.d.ts +55 -0
  105. package/dist/types/templates/template-registry.d.ts +63 -0
  106. package/dist/types/testing.d.ts +168 -0
  107. package/dist/types/utils/embed-query.d.ts +20 -0
  108. package/dist/types/utils/logger.d.ts +50 -0
  109. package/dist/types/utils/rate-limiter.d.ts +66 -0
  110. package/dist/types/utils/retry.d.ts +27 -0
  111. package/dist/types/utils/run-abort.d.ts +34 -0
  112. package/dist/types/utils/secure-logging.d.ts +51 -0
  113. package/dist/types/utils/structured-logger.d.ts +126 -0
  114. package/package.json +72 -33
  115. package/src/adapters/ai-sdk.js +215 -0
  116. package/src/agentic/agentic-pipeline.js +75 -7
  117. package/src/agentic/iterative-retriever.js +47 -10
  118. package/src/agentic/query-planner.js +329 -78
  119. package/src/cache/component-keys.js +38 -0
  120. package/src/cache/memory-cache.js +87 -0
  121. package/src/cache/pipeline-cache.js +260 -0
  122. package/src/cache/redis-cache.js +83 -0
  123. package/src/citation/citation-tracker.js +6 -4
  124. package/src/citation/hallucination-detector.js +2 -2
  125. package/src/citation/source-mapper.js +15 -28
  126. package/src/cli/commands/ai.js +106 -0
  127. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  128. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  129. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  130. package/src/cli/commands/plugin-hub.js +114 -49
  131. package/src/cli/diagnostics/config-checker.js +76 -63
  132. package/src/cli/diagnostics/environment-checker.js +62 -31
  133. package/src/cli/diagnostics/plugin-checker.js +36 -89
  134. package/src/cli/doctor-command.js +39 -5
  135. package/src/cli/enhanced-cli-commands.js +104 -233
  136. package/src/cli/handlers/ai-handler.js +364 -0
  137. package/src/cli/handlers/diagnostics-handler.js +78 -31
  138. package/src/cli/handlers/ingest-handler.js +28 -22
  139. package/src/cli/handlers/init-handler.js +148 -25
  140. package/src/cli/handlers/query-handler.js +203 -25
  141. package/src/cli/handlers/run-handler.js +16 -167
  142. package/src/cli/handlers/serve-handler.js +60 -0
  143. package/src/cli/services/bench-stats.js +54 -0
  144. package/src/cli/services/benchmark.js +161 -0
  145. package/src/cli/services/feedback.js +155 -0
  146. package/src/cli/services/instrument.js +83 -0
  147. package/src/cli/services/rag-service.js +361 -0
  148. package/src/cli/shared/cli-utils.js +24 -7
  149. package/src/cli/shared/config-context.js +21 -0
  150. package/src/cli/wizard/config-builder.js +81 -0
  151. package/src/cli/wizard/init-wizard.js +602 -0
  152. package/src/config/component-factory.js +125 -0
  153. package/src/config/enhanced-ragrc-schema.js +0 -2
  154. package/src/config/load-config.js +29 -10
  155. package/src/config/load-plugin-config.js +46 -2
  156. package/src/config/validate-schema.js +4 -1
  157. package/src/connectors/anthropic-connector.js +115 -57
  158. package/src/connectors/base-connector.js +74 -19
  159. package/src/connectors/cohere-connector.js +108 -34
  160. package/src/connectors/connector-utils.js +302 -0
  161. package/src/connectors/local-embedder.js +63 -5
  162. package/src/connectors/memory-retriever.js +125 -14
  163. package/src/connectors/ollama-connector.js +112 -51
  164. package/src/connectors/openai-connector.js +153 -44
  165. package/src/connectors/rag-prompt.js +88 -0
  166. package/src/core/create-pipeline.js +307 -61
  167. package/src/core/performance/parallel-processor.js +62 -1
  168. package/src/core/plugin-registry.js +125 -26
  169. package/src/core/stage-contracts.js +36 -0
  170. package/src/core/validate-plugin.js +85 -0
  171. package/src/cost/cost-calculator.js +205 -32
  172. package/src/debug/execution-tracer.js +14 -4
  173. package/src/dev/dev-server.js +173 -11
  174. package/src/dev/hot-reload.js +44 -4
  175. package/src/ecosystem/plugin-checks.js +181 -0
  176. package/src/ecosystem/plugin-hub.js +50 -125
  177. package/src/ecosystem/plugin-installer.js +298 -0
  178. package/src/ecosystem/plugin-probe.js +127 -0
  179. package/src/ecosystem/plugin-scanner.js +185 -0
  180. package/src/ecosystem/tar-extract.js +114 -0
  181. package/src/enterprise/audit-logging.js +158 -29
  182. package/src/enterprise/sso-integration.js +716 -228
  183. package/src/evaluate/context-metrics.js +28 -16
  184. package/src/evaluate/evaluator.js +3 -3
  185. package/src/evaluate/faithfulness.js +18 -14
  186. package/src/evaluate/groundedness.js +43 -12
  187. package/src/evaluate/pipeline-evaluator.js +35 -8
  188. package/src/evaluate/scoring.js +251 -3
  189. package/src/evaluate/semantic-similarity.js +79 -0
  190. package/src/experimental.js +71 -0
  191. package/src/grounding/check-answer.js +173 -0
  192. package/src/guardrails/guardrails-pipeline.js +69 -7
  193. package/src/guardrails/injection-detector.js +283 -0
  194. package/src/guardrails/post-generation-guard.js +19 -0
  195. package/src/guardrails/pre-retrieval-guard.js +36 -36
  196. package/src/guardrails/retrieval-guard.js +58 -8
  197. package/src/index.js +58 -1
  198. package/src/loader/csv-loader.js +3 -2
  199. package/src/loader/document-reader.js +91 -0
  200. package/src/loader/html-loader.js +3 -1
  201. package/src/loader/markdown-loader.js +3 -2
  202. package/src/reranker/cascade-reranker.js +3 -0
  203. package/src/reranker/embedding-reranker.js +136 -21
  204. package/src/reranker/llm-reranker.js +79 -12
  205. package/src/reranker/scoring-reranker.js +3 -0
  206. package/src/retrieval/bm25-search.js +12 -6
  207. package/src/retrieval/file-vector-store.js +241 -0
  208. package/src/retrieval/hybrid-retriever.js +22 -19
  209. package/src/security/jwt-config.js +13 -2
  210. package/src/security/jwt-validator.js +139 -34
  211. package/src/security/plugin-sandbox.js +18 -1
  212. package/src/security/plugin-signature-verifier.js +245 -70
  213. package/src/testing.js +254 -0
  214. package/src/utils/deprecate.js +69 -0
  215. package/src/utils/embed-query.js +33 -0
  216. package/src/utils/input-sanitizer.js +19 -4
  217. package/src/utils/logger.js +34 -4
  218. package/src/utils/run-abort.js +102 -0
  219. package/src/ai/federated-learning.js +0 -211
  220. package/src/ai/federation.js +0 -71
  221. package/src/ai/index.js +0 -22
  222. package/src/ai/model-trainer.js +0 -217
  223. package/src/ai/model-training.js +0 -202
  224. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  225. package/src/ai/orchestrator.js +0 -65
  226. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  227. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  228. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  229. package/src/cli/commands/ai-ml/index.js +0 -17
  230. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  231. package/src/cli/commands/ai-ml/shared.js +0 -51
  232. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  233. package/src/cli/commands/ai-ml.js +0 -386
  234. package/src/cli/commands/docs.js +0 -228
  235. package/src/cli/commands/dx.js +0 -538
  236. package/src/cli/commands/wizard.js +0 -627
  237. package/src/cli/input-sanitization-middleware.js +0 -356
  238. package/src/cli/interactive-wizard.js +0 -373
  239. package/src/cli/templates/development.template.json +0 -64
  240. package/src/cli/templates/minimal.template.json +0 -33
  241. package/src/cli/templates/production.template.json +0 -104
  242. package/src/cli/templates/testing.template.json +0 -45
  243. package/src/cli/wizard/configuration.js +0 -148
  244. package/src/cli/wizard/observability.js +0 -79
  245. package/src/cli/wizard/performance.js +0 -76
  246. package/src/cli/wizard/plugin-selection.js +0 -297
  247. package/src/cli/wizard/preview-save.js +0 -172
  248. package/src/cli/wizard/project-setup.js +0 -66
  249. package/src/core/pipeline-factory.js +0 -59
  250. package/src/dag/enhanced-dag-engine.js +0 -894
  251. package/src/dx/index.js +0 -40
  252. package/src/dx/integration-templates.js +0 -662
  253. package/src/dx/performance-profiler.js +0 -968
  254. package/src/dx/realtime-debugger.js +0 -755
  255. package/src/dx/visual-pipeline-builder.js +0 -578
  256. package/src/ingest.js +0 -59
  257. package/src/pipeline/pipeline.js +0 -18
  258. package/src/query.js +0 -117
@@ -1,23 +1,161 @@
1
1
  'use strict';
2
- const { PluginRegistry } = require('./plugin-registry.js');
2
+ // Share the exported singleton so plugins registered via
3
+ // `pluginRegistry.register()` are resolvable by name here.
4
+ const defaultRegistry = require('./plugin-registry.js');
5
+ const { embedQuery, canEmbed } = require('../utils/embed-query');
6
+ const {
7
+ normalizeCacheOption,
8
+ PipelineCache,
9
+ } = require('../cache/pipeline-cache');
10
+ const { createRunAbort } = require('../utils/run-abort');
3
11
 
4
- const defaultRegistry = new PluginRegistry();
12
+ /** Metrics that only need the query, the retrieved docs and a reference. */
13
+ const RETRIEVAL_METRICS = ['contextPrecision', 'contextRecall'];
5
14
 
6
15
  function resolve(registry, type, value) {
7
16
  const reg = registry || defaultRegistry;
8
17
  return typeof value === 'string' ? reg.get(type, value) : value;
9
18
  }
10
19
 
11
- function withTimeout(promise, ms) {
12
- if (!ms) return promise;
13
- return Promise.race([
14
- promise,
15
- new Promise((_, rej) =>
16
- setTimeout(() => rej(new Error('Execution timeout')), ms),
17
- ),
18
- ]);
20
+ /**
21
+ * Citations and evaluation for one run. `answer` is null when no llm is
22
+ * configured: citations are then skipped and evaluation is limited to the
23
+ * retrieval metrics (context precision, and context recall when a reference
24
+ * answer is given).
25
+ *
26
+ * @param {object} params
27
+ * @param {string} params.query
28
+ * @param {string|null} params.answer
29
+ * @param {Array<object>} params.results
30
+ * @param {object} params.options - Run options
31
+ * @returns {{ citations?: object, evaluation?: object }}
32
+ */
33
+ function computeQuality({ query, answer, results, options }) {
34
+ const out = {};
35
+ const docs = Array.isArray(results) ? results : [];
36
+
37
+ if (options.citations && answer !== null) {
38
+ const { CitationTracker } = require('../citation/citation-tracker');
39
+ const tracker = new CitationTracker(options.citationOptions || {});
40
+ out.citations = tracker.track(answer, docs);
41
+ }
42
+
43
+ if (options.evaluate) {
44
+ const {
45
+ PipelineEvaluator,
46
+ DEFAULT_CONFIG,
47
+ } = require('../evaluate/pipeline-evaluator');
48
+ const evaluateOptions = options.evaluateOptions || {};
49
+ let config = evaluateOptions;
50
+ if (answer === null) {
51
+ const metrics = evaluateOptions.metrics || DEFAULT_CONFIG.metrics;
52
+ config = {
53
+ ...evaluateOptions,
54
+ metrics: metrics.filter((m) => RETRIEVAL_METRICS.includes(m)),
55
+ };
56
+ }
57
+ const evaluator = new PipelineEvaluator(config);
58
+ out.evaluation = evaluator.evaluate(
59
+ { query, answer: answer ?? '', results: docs },
60
+ {
61
+ citationResult: out.citations,
62
+ referenceAnswer:
63
+ options.referenceAnswer ?? evaluateOptions.referenceAnswer,
64
+ },
65
+ );
66
+ }
67
+
68
+ return out;
69
+ }
70
+
71
+ /**
72
+ * Pass a token stream through, stopping when the run's signal aborts. When
73
+ * citations or evaluation were requested, the streamed answer is accumulated
74
+ * and the stream ends with one final chunk
75
+ * `{ token: '', done: true, citations?, evaluation? }` (the source's own done
76
+ * chunk is replaced by it; a token it carried is emitted first).
77
+ *
78
+ * @param {AsyncIterable<object|string>} source
79
+ * @param {object} params
80
+ * @param {AbortSignal} params.signal
81
+ * @param {((text: string) => object)|null} params.finalize
82
+ * @param {() => void} params.onEnd
83
+ */
84
+ async function* guardStream(source, { signal, finalize, onEnd }) {
85
+ try {
86
+ let text = '';
87
+ for await (const chunk of source) {
88
+ if (signal.aborted) throw signal.reason;
89
+ if (!finalize) {
90
+ yield chunk;
91
+ continue;
92
+ }
93
+ const token =
94
+ typeof chunk === 'string' ? chunk : String(chunk?.token ?? '');
95
+ text += token;
96
+ if (chunk && chunk.done) {
97
+ if (token) yield { ...chunk, token, done: false };
98
+ break;
99
+ }
100
+ yield chunk;
101
+ }
102
+ if (signal.aborted) throw signal.reason;
103
+ if (finalize) yield { token: '', done: true, ...finalize(text) };
104
+ } finally {
105
+ onEnd();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Pass a stream through and report its text when it completed: it reached a
111
+ * done chunk or its end, and the run was not aborted. A stream the consumer
112
+ * stops reading early is not reported, so a partial answer is never cached.
113
+ */
114
+ async function* reportWhenComplete(source, signal, onComplete) {
115
+ let text = '';
116
+ let completed = false;
117
+ try {
118
+ for await (const chunk of source) {
119
+ text += typeof chunk === 'string' ? chunk : String(chunk?.token ?? '');
120
+ if (chunk && chunk.done) completed = true;
121
+ yield chunk;
122
+ }
123
+ completed = true;
124
+ } finally {
125
+ if (completed && !signal.aborted) await onComplete(text);
126
+ }
19
127
  }
20
128
 
129
+ /**
130
+ * Create a RAG pipeline: (embed) -> retrieve -> (filter) -> (rerank) ->
131
+ * generate -> (citations, evaluation).
132
+ *
133
+ * `run({ query, queryVector, options })` options:
134
+ * - `topK` (default 3)
135
+ * - `timeout` (ms): aborts the run; resolves `{ success: false, error: 'Execution timeout' }`
136
+ * - `signal` (AbortSignal): caller cancellation; resolves
137
+ * `{ success: false, error: 'Execution aborted' }` when it aborts
138
+ * - `stream`: return an async iterable of `{ token, done }` chunks
139
+ * - `citations`, `citationOptions`, `evaluate`, `evaluateOptions`,
140
+ * `referenceAnswer` (ground truth for context recall)
141
+ * - `filterResults(docs)`: hook applied after retrieval
142
+ *
143
+ * `cache` (factory option): `true`, a CacheBackend, or
144
+ * `{ embeddings, retrieval, generation, ttlSeconds }` - see
145
+ * src/cache/pipeline-cache.js. Results then carry `cache: { embeddings,
146
+ * retrieval, generation }` ('hit', 'miss' or 'skip'), and the pipeline has
147
+ * `cacheStats()`.
148
+ *
149
+ * Every component call receives the run's AbortSignal: `retriever.retrieve({
150
+ * ..., signal })`, `embedder.embedQuery(text, { signal })` /
151
+ * `embedder.embed(text, { signal })`, `reranker.rerank(query, docs, { topK,
152
+ * signal })` and `llm.generate(query, docs, { ...options, signal })`.
153
+ * Components that ignore it still work; the run settles on abort anyway and
154
+ * no later stage starts.
155
+ *
156
+ * Without an llm the pipeline is retrieval-only: `answer` is null, citations
157
+ * are skipped and evaluation covers the retrieval metrics only.
158
+ */
21
159
  module.exports.createRagPipeline = function createRagPipeline({
22
160
  registry,
23
161
  loader,
@@ -25,6 +163,7 @@ module.exports.createRagPipeline = function createRagPipeline({
25
163
  retriever,
26
164
  llm,
27
165
  reranker,
166
+ cache,
28
167
  } = {}) {
29
168
  const reg = registry || defaultRegistry;
30
169
  const loaderI = loader ? resolve(reg, 'loader', loader) : null;
@@ -36,84 +175,191 @@ module.exports.createRagPipeline = function createRagPipeline({
36
175
  return [];
37
176
  },
38
177
  };
39
- const llmI = llm
40
- ? resolve(reg, 'llm', llm)
41
- : {
42
- async generate() {
43
- return 'ok';
44
- },
45
- };
178
+ const llmI = llm ? resolve(reg, 'llm', llm) : null;
46
179
  const rerankerI = reranker ? resolve(reg, 'reranker', reranker) : null;
47
180
 
48
- async function runOnce({ query, queryVector, options = {} }) {
49
- const { timeout, stream } = options || {};
181
+ const cacheLayers = normalizeCacheOption(cache);
182
+ const pc = cacheLayers
183
+ ? new PipelineCache(cacheLayers, {
184
+ embedder: embedderI,
185
+ // The default stand-in retriever returns nothing; never cache it.
186
+ retriever: retriever ? retrieverI : null,
187
+ llm: llmI,
188
+ })
189
+ : null;
190
+
191
+ async function runOnce({ query, queryVector, options = {} } = {}) {
192
+ const opts = options || {};
193
+ const { timeout, stream } = opts;
194
+ const run = createRunAbort({ timeout, signal: opts.signal });
195
+ const { signal } = run;
196
+ let streaming = false;
197
+
198
+ // Per-run cache outcome per layer: 'hit', 'miss' or 'skip'.
199
+ const cacheStatus = {};
200
+ const withCache = (result) =>
201
+ pc ? { ...result, cache: { ...cacheStatus } } : result;
202
+
50
203
  const doWork = (async () => {
51
- if (loaderI?.load) await loaderI.load();
52
- if (embedderI?.embed && query) await embedderI.embed(query);
204
+ run.throwIfAborted();
205
+ if (loaderI?.load) {
206
+ await loaderI.load();
207
+ run.throwIfAborted();
208
+ }
209
+ let vector = queryVector;
210
+ if (!vector && query && canEmbed(embedderI)) {
211
+ const key = pc && pc.embeddingKey(query);
212
+ vector = pc && (await pc.get('embeddings', key, cacheStatus));
213
+ if (!vector) {
214
+ vector = await embedQuery(embedderI, query, { signal });
215
+ run.throwIfAborted();
216
+ if (pc) await pc.set('embeddings', key, vector);
217
+ }
218
+ run.throwIfAborted();
219
+ }
53
220
 
54
- const docs = await retrieverI.retrieve({
55
- query,
56
- queryVector,
57
- topK: options.topK ?? 3,
58
- });
221
+ // Raw retriever output is cached before filterResults (guardrail ACLs)
222
+ // and the reranker, so both still run on every request.
223
+ const topK = opts.topK ?? 3;
224
+ const retrievalKey =
225
+ pc && pc.retrievalKey({ query, queryVector: vector, topK });
226
+ let docs = pc && (await pc.get('retrieval', retrievalKey, cacheStatus));
227
+ if (!docs) {
228
+ docs = await retrieverI.retrieve({
229
+ query,
230
+ queryVector: vector,
231
+ topK,
232
+ signal,
233
+ });
234
+ run.throwIfAborted();
235
+ if (pc && Array.isArray(docs)) {
236
+ await pc.set('retrieval', retrievalKey, docs);
237
+ }
238
+ }
239
+ run.throwIfAborted();
240
+ // Hook for filtering (e.g. ACL guardrails) before anything downstream,
241
+ // including the LLM, sees the retrieved documents.
242
+ if (typeof opts.filterResults === 'function' && Array.isArray(docs)) {
243
+ docs = await opts.filterResults(docs);
244
+ run.throwIfAborted();
245
+ }
59
246
  let results = docs;
60
247
  if (rerankerI?.rerank && Array.isArray(docs) && docs.length) {
61
248
  results = await rerankerI.rerank(query || '', docs, {
62
- topK: options.topK ?? docs.length,
249
+ topK: opts.topK ?? docs.length,
250
+ signal,
63
251
  });
252
+ run.throwIfAborted();
64
253
  }
65
254
 
255
+ const genOptions = { ...opts, signal };
256
+ const wantsQuality = Boolean(opts.citations || opts.evaluate);
257
+ const generationKey =
258
+ pc && llmI
259
+ ? pc.generationKey({ query, docs: results, options: opts })
260
+ : null;
261
+ const cachedAnswer =
262
+ pc && llmI
263
+ ? await pc.get('generation', generationKey, cacheStatus)
264
+ : undefined;
265
+ run.throwIfAborted();
266
+
66
267
  if (stream) {
67
- // Use streaming generate if available
68
- if (llmI.generateStream) {
69
- const streamResult = llmI.generateStream(query, results, options);
70
- return streamResult;
268
+ let source;
269
+ let finalize = wantsQuality
270
+ ? (text) =>
271
+ computeQuality({ query, answer: text, results, options: opts })
272
+ : null;
273
+ if (!llmI) {
274
+ // Retrieval only: no tokens, one done chunk carrying the results.
275
+ const quality = computeQuality({
276
+ query,
277
+ answer: null,
278
+ results,
279
+ options: opts,
280
+ });
281
+ source = (async function* noAnswer() {})();
282
+ finalize = () => ({ results, ...quality });
283
+ } else if (typeof cachedAnswer === 'string') {
284
+ source = (async function* cached() {
285
+ yield { token: cachedAnswer, done: false };
286
+ yield { token: '', done: true };
287
+ })();
288
+ } else if (llmI.generateStream) {
289
+ source = await llmI.generateStream(query, results, genOptions);
71
290
  } else if (llmI.generate) {
72
291
  // Fallback: simulate streaming from regular generate
73
- const answer = (await llmI.generate(query, results, options)) ?? '';
74
- async function* gen() {
75
- const chunks = String(answer || '').length
76
- ? [String(answer)]
77
- : [''];
78
- for (const c of chunks) yield { token: c, done: false };
292
+ const answer =
293
+ (await llmI.generate(query, results, genOptions)) ?? '';
294
+ source = (async function* gen() {
295
+ yield { token: String(answer || ''), done: false };
79
296
  yield { token: '', done: true };
297
+ })();
298
+ }
299
+ if (source) {
300
+ run.throwIfAborted();
301
+ streaming = true;
302
+ if (pc && llmI && generationKey && typeof cachedAnswer !== 'string') {
303
+ source = reportWhenComplete(source, signal, (text) =>
304
+ pc.set('generation', generationKey, text),
305
+ );
80
306
  }
81
- return gen();
307
+ return guardStream(source, {
308
+ signal,
309
+ finalize,
310
+ onEnd: run.detach,
311
+ });
82
312
  }
83
313
  }
84
314
 
85
- const answer = (await llmI.generate?.(query, results, options)) ?? '';
86
- // tiny delay so durations > 0 in timing tests
87
- await new Promise((r) => setTimeout(r, 5));
88
-
89
- const result = { success: true, query, results };
90
-
91
- if (options.citations) {
92
- const { CitationTracker } = require('../citation/citation-tracker');
93
- const tracker = new CitationTracker(options.citationOptions || {});
94
- result.answer = String(answer);
95
- result.citations = tracker.track(String(answer), results);
315
+ if (!llmI) {
316
+ return withCache({
317
+ success: true,
318
+ query,
319
+ results,
320
+ answer: null,
321
+ ...computeQuality({ query, answer: null, results, options: opts }),
322
+ });
96
323
  }
97
324
 
98
- if (options.evaluate) {
99
- const { PipelineEvaluator } = require('../evaluate/pipeline-evaluator');
100
- const evaluator = new PipelineEvaluator(options.evaluateOptions || {});
101
- result.answer = result.answer || String(answer);
102
- result.evaluation = evaluator.evaluate(
103
- { query, answer: String(answer), results },
104
- { citationResult: result.citations },
105
- );
325
+ let answer;
326
+ if (typeof cachedAnswer === 'string') {
327
+ answer = cachedAnswer;
328
+ } else {
329
+ const raw = (await llmI.generate?.(query, results, genOptions)) ?? '';
330
+ run.throwIfAborted();
331
+ answer = String(raw);
332
+ if (pc) await pc.set('generation', generationKey, answer);
333
+ run.throwIfAborted();
106
334
  }
107
335
 
108
- return result;
336
+ return withCache({
337
+ success: true,
338
+ query,
339
+ results,
340
+ answer,
341
+ ...computeQuality({ query, answer, results, options: opts }),
342
+ });
109
343
  })();
110
344
 
111
345
  try {
112
- return await withTimeout(doWork, timeout);
346
+ return await run.race(doWork);
113
347
  } catch (e) {
114
- return { success: false, error: String(e.message || e) };
348
+ return { success: false, error: String(e?.message || e) };
349
+ } finally {
350
+ // The timeout covers the run up to the returned stream; a caller
351
+ // signal keeps cancelling a stream until it finishes.
352
+ run.clearTimer();
353
+ if (!streaming) run.detach();
115
354
  }
116
355
  }
117
356
 
118
- return { run: runOnce, cleanup() {} };
357
+ return {
358
+ run: runOnce,
359
+ cleanup() {},
360
+ /** Cache hits, misses and skips per layer; null when caching is off. */
361
+ cacheStats() {
362
+ return pc ? pc.stats() : null;
363
+ },
364
+ };
119
365
  };
@@ -36,16 +36,77 @@ class Semaphore {
36
36
  }
37
37
  }
38
38
 
39
+ /** Built-in embedders whose embed() takes one string, not an array. */
40
+ function isSingleTextEmbedder(embedder) {
41
+ /* eslint-disable global-require */
42
+ const singleText = [
43
+ require('../../connectors/local-embedder').LocalEmbedder,
44
+ require('../../connectors/openai-connector').OpenAIConnector,
45
+ require('../../connectors/ollama-connector').OllamaConnector,
46
+ require('../../connectors/cohere-connector').CohereConnector,
47
+ ];
48
+ /* eslint-enable global-require */
49
+ return singleText.some((Class) => Class && embedder instanceof Class);
50
+ }
51
+
52
+ const EMBED_MODES = ['auto', 'batch', 'single'];
53
+
39
54
  /**
40
55
  * Parallel processor for embedding operations with configurable concurrency
41
56
  */
42
57
  class ParallelEmbedder {
58
+ /**
59
+ * @param {object} embedder - Embedder with embed() and/or embedBatch()
60
+ * @param {object} [_options]
61
+ * @param {number} [_options.batchSize=10] - Texts per batch
62
+ * @param {number} [_options.maxConcurrency=3] - Batches in flight at once
63
+ * @param {number} [_options.retryAttempts=2] - Retries per failed batch
64
+ * @param {number} [_options.retryDelay=1000] - Base retry delay in ms
65
+ * @param {'auto'|'batch'|'single'} [_options.mode='auto'] - How a batch is
66
+ * embedded: 'batch' calls embed(texts) and expects one vector per text,
67
+ * 'single' calls embed(text) for each text. 'auto' uses embedBatch(texts)
68
+ * when the embedder has it, 'single' for the built-in connectors (which
69
+ * embed one string at a time) and 'batch' otherwise.
70
+ */
43
71
  constructor(embedder, _options = {}) {
44
72
  this.embedder = embedder;
45
73
  this.batchSize = _options.batchSize || 10;
46
74
  this.maxConcurrency = _options.maxConcurrency || 3;
47
75
  this.retryAttempts = _options.retryAttempts || 2;
48
76
  this.retryDelay = _options.retryDelay || 1000;
77
+ const mode = _options.mode || 'auto';
78
+ if (!EMBED_MODES.includes(mode)) {
79
+ throw new Error(
80
+ `Invalid mode "${mode}". Expected one of: ${EMBED_MODES.join(', ')}`,
81
+ );
82
+ }
83
+ this.mode = mode;
84
+ }
85
+
86
+ /**
87
+ * Embed one batch according to the mode.
88
+ * @param {string[]} batch
89
+ * @returns {Promise<number[][]>}
90
+ * @private
91
+ */
92
+ async _embed(batch) {
93
+ const embedder = this.embedder;
94
+ if (this.mode === 'auto' && typeof embedder.embedBatch === 'function') {
95
+ return embedder.embedBatch(batch);
96
+ }
97
+ const single =
98
+ this.mode === 'single' ||
99
+ (this.mode === 'auto' && isSingleTextEmbedder(embedder));
100
+ if (!single) {
101
+ return embedder.embed(batch);
102
+ }
103
+ // One text at a time within a batch, so maxConcurrency still bounds the
104
+ // number of requests in flight.
105
+ const vectors = [];
106
+ for (const text of batch) {
107
+ vectors.push(await embedder.embed(text));
108
+ }
109
+ return vectors;
49
110
  }
50
111
 
51
112
  /**
@@ -133,7 +194,7 @@ class ParallelEmbedder {
133
194
 
134
195
  for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
135
196
  try {
136
- const result = await this.embedder.embed(batch);
197
+ const result = await this._embed(batch);
137
198
 
138
199
  // Validate result
139
200
  if (!Array.isArray(result) || result.length !== batch.length) {