@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
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Caching for createRagPipeline({ cache }): three opt-in layers.
5
+ *
6
+ * - embeddings: the query vector, keyed by the embedder's identity and the
7
+ * query text.
8
+ * - retrieval: the retriever's raw results, keyed by the retriever's state,
9
+ * the query and topK. Cached before `filterResults` (guardrail ACLs) and
10
+ * the reranker, so those still run on every request and one user's
11
+ * permissions can never shape another user's results.
12
+ * - generation: the answer, keyed by the llm's identity, the query, the
13
+ * exact documents it was given and the generation options. Skipped when
14
+ * the temperature is above 0.
15
+ *
16
+ * A layer caches only when its component identifies itself through
17
+ * `getCacheKey(role)` or a `cacheKey` string (the built-ins do), so a cache
18
+ * can never return results for a different model, index or configuration.
19
+ * Only successful, completed work is stored: nothing from an aborted or
20
+ * timed-out run.
21
+ */
22
+
23
+ const crypto = require('crypto');
24
+ const { MemoryCache } = require('./memory-cache');
25
+
26
+ const LAYERS = ['embeddings', 'retrieval', 'generation'];
27
+ const DEFAULT_TTL_SECONDS = {
28
+ embeddings: 30 * 24 * 3600,
29
+ retrieval: 3600,
30
+ generation: 24 * 3600,
31
+ };
32
+ const ROLE = {
33
+ embeddings: 'embedder',
34
+ retrieval: 'retriever',
35
+ generation: 'llm',
36
+ };
37
+
38
+ /** Generation options that change the answer and so belong in its key. */
39
+ const GENERATION_OPTIONS = [
40
+ 'temperature',
41
+ 'maxTokens',
42
+ 'maxOutputTokens',
43
+ 'topP',
44
+ 'model',
45
+ 'systemPrompt',
46
+ 'seed',
47
+ 'stopSequences',
48
+ ];
49
+
50
+ function hash(value) {
51
+ return crypto
52
+ .createHash('sha256')
53
+ .update(JSON.stringify(value))
54
+ .digest('hex');
55
+ }
56
+
57
+ function isBackend(value) {
58
+ return (
59
+ value && typeof value.get === 'function' && typeof value.set === 'function'
60
+ );
61
+ }
62
+
63
+ /** A component's identity for a role, or null when it has none. */
64
+ function componentKey(component, role) {
65
+ if (!component) return null;
66
+ if (typeof component.getCacheKey === 'function') {
67
+ const key = component.getCacheKey(role);
68
+ return typeof key === 'string' && key ? key : null;
69
+ }
70
+ return typeof component.cacheKey === 'string' && component.cacheKey
71
+ ? component.cacheKey
72
+ : null;
73
+ }
74
+
75
+ const warned = new Set();
76
+ function warnOnce(code, message) {
77
+ if (warned.has(code)) return;
78
+ warned.add(code);
79
+ process.emitWarning(message, { type: 'RagCacheWarning', code });
80
+ }
81
+
82
+ /**
83
+ * Normalise the `cache` option of createRagPipeline.
84
+ *
85
+ * - `true`: all three layers in one in-memory cache
86
+ * - a CacheBackend: all three layers in it
87
+ * - `{ embeddings, retrieval, generation, ttlSeconds }`: per layer, each
88
+ * `true` (in-memory), `false`/absent (off) or a CacheBackend;
89
+ * `ttlSeconds: { embeddings, retrieval, generation }` overrides the
90
+ * defaults (30 days, 1 hour, 24 hours)
91
+ *
92
+ * @returns {Object|null} Layer config, or null when caching is off
93
+ */
94
+ function normalizeCacheOption(option) {
95
+ if (!option) return null;
96
+ const shared =
97
+ option === true ? new MemoryCache() : isBackend(option) ? option : null;
98
+ const layers = {};
99
+ for (const layer of LAYERS) {
100
+ let backend = null;
101
+ if (shared) {
102
+ backend = shared;
103
+ } else if (option[layer] === true) {
104
+ backend = new MemoryCache();
105
+ } else if (isBackend(option[layer])) {
106
+ backend = option[layer];
107
+ }
108
+ if (backend) {
109
+ layers[layer] = {
110
+ backend,
111
+ ttlSeconds:
112
+ (option.ttlSeconds && option.ttlSeconds[layer]) ??
113
+ DEFAULT_TTL_SECONDS[layer],
114
+ hits: 0,
115
+ misses: 0,
116
+ skipped: 0,
117
+ };
118
+ }
119
+ }
120
+ return Object.keys(layers).length ? layers : null;
121
+ }
122
+
123
+ /**
124
+ * Per-pipeline cache. Every method returns undefined and does nothing for a
125
+ * layer that is off or whose component has no cache key.
126
+ */
127
+ class PipelineCache {
128
+ /**
129
+ * @param {Object} layers - normalizeCacheOption() output
130
+ * @param {{ embedder?: Object, retriever?: Object, llm?: Object }} components
131
+ */
132
+ constructor(layers, components) {
133
+ this.layers = layers;
134
+ this.components = components;
135
+ }
136
+
137
+ /** @private */
138
+ _key(layer, parts) {
139
+ const config = this.layers[layer];
140
+ if (!config) return null;
141
+ const component = this.components[ROLE[layer]];
142
+ const id = componentKey(component, ROLE[layer]);
143
+ if (!id) {
144
+ config.skipped += 1;
145
+ warnOnce(
146
+ `RAG_CACHE_NO_KEY_${layer.toUpperCase()}`,
147
+ `The ${layer} cache is off for this ${ROLE[layer]}: it has no getCacheKey(role) method or cacheKey string, so cached results could not be told apart from another configuration's. Add one to enable it.`,
148
+ );
149
+ return null;
150
+ }
151
+ return `${layer}:${hash([id, parts])}`;
152
+ }
153
+
154
+ /** @private */
155
+ async _get(layer, key, status) {
156
+ if (!key) {
157
+ status[layer] = 'skip';
158
+ return undefined;
159
+ }
160
+ const config = this.layers[layer];
161
+ let value;
162
+ try {
163
+ value = await config.backend.get(key);
164
+ } catch (error) {
165
+ // A cache outage must not fail the run.
166
+ warnOnce('RAG_CACHE_READ_FAILED', `Cache read failed: ${error.message}`);
167
+ value = undefined;
168
+ }
169
+ if (value === undefined) {
170
+ config.misses += 1;
171
+ status[layer] = 'miss';
172
+ } else {
173
+ config.hits += 1;
174
+ status[layer] = 'hit';
175
+ }
176
+ return value;
177
+ }
178
+
179
+ /** @private */
180
+ async _set(layer, key, value) {
181
+ if (!key || value === undefined) return;
182
+ const config = this.layers[layer];
183
+ try {
184
+ await config.backend.set(key, value, { ttlSeconds: config.ttlSeconds });
185
+ } catch (error) {
186
+ warnOnce(
187
+ 'RAG_CACHE_WRITE_FAILED',
188
+ `Cache write failed: ${error.message}`,
189
+ );
190
+ }
191
+ }
192
+
193
+ embeddingKey(query) {
194
+ return this._key('embeddings', { query });
195
+ }
196
+
197
+ retrievalKey({ query, queryVector, topK }) {
198
+ // Keyed on the query vector, not just the text, so a different embedder
199
+ // (or a retrained one) never gets another embedder's results; the text
200
+ // stays in the key for keyword and hybrid retrievers.
201
+ return this._key('retrieval', {
202
+ query: query ?? null,
203
+ vector: queryVector ? hash(queryVector) : null,
204
+ topK,
205
+ });
206
+ }
207
+
208
+ /**
209
+ * @returns {string|null} The key, or null to skip (temperature above 0,
210
+ * or no llm cache key)
211
+ */
212
+ generationKey({ query, docs, options }) {
213
+ if (!this.layers.generation) return null;
214
+ const llm = this.components.llm;
215
+ const temperature =
216
+ options.temperature ?? (llm && llm.config && llm.config.temperature);
217
+ if (typeof temperature === 'number' && temperature > 0) {
218
+ this.layers.generation.skipped += 1;
219
+ return null;
220
+ }
221
+ const generationOptions = {};
222
+ for (const name of GENERATION_OPTIONS) {
223
+ if (options[name] !== undefined) generationOptions[name] = options[name];
224
+ }
225
+ const context = (docs || []).map((d) =>
226
+ typeof d === 'string' ? d : [d.id ?? null, d.content ?? d.text ?? ''],
227
+ );
228
+ return this._key('generation', {
229
+ query,
230
+ context: hash(context),
231
+ options: generationOptions,
232
+ });
233
+ }
234
+
235
+ get(layer, key, status) {
236
+ if (!this.layers[layer]) return Promise.resolve(undefined);
237
+ return this._get(layer, key, status);
238
+ }
239
+
240
+ set(layer, key, value) {
241
+ if (!this.layers[layer]) return Promise.resolve();
242
+ return this._set(layer, key, value);
243
+ }
244
+
245
+ /** Hits, misses and skips per layer since the pipeline was created. */
246
+ stats() {
247
+ const out = {};
248
+ for (const [layer, c] of Object.entries(this.layers)) {
249
+ out[layer] = { hits: c.hits, misses: c.misses, skipped: c.skipped };
250
+ }
251
+ return out;
252
+ }
253
+ }
254
+
255
+ module.exports = {
256
+ normalizeCacheOption,
257
+ PipelineCache,
258
+ componentKey,
259
+ DEFAULT_TTL_SECONDS,
260
+ };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CacheBackend on Redis. Pass a connected client from `redis` (node-redis 4
5
+ * and later) or `ioredis`; neither is a dependency of this package.
6
+ *
7
+ * Values are stored as JSON under `prefix + key`, with a millisecond expiry
8
+ * when a time to live applies.
9
+ */
10
+ class RedisCache {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {Object} options.client - Connected node-redis or ioredis client
14
+ * @param {string} [options.prefix='rag:'] - Key prefix
15
+ * @param {number} [options.ttlSeconds] - Default time to live
16
+ */
17
+ constructor({ client, prefix = 'rag:', ttlSeconds } = {}) {
18
+ if (
19
+ !client ||
20
+ typeof client.get !== 'function' ||
21
+ typeof client.set !== 'function'
22
+ ) {
23
+ throw new Error(
24
+ 'RedisCache needs a connected client from "redis" or "ioredis"',
25
+ );
26
+ }
27
+ this.client = client;
28
+ this.prefix = prefix;
29
+ this.ttlSeconds = ttlSeconds;
30
+ // node-redis 4+ exposes isOpen and takes set(key, value, { PX }); ioredis
31
+ // takes set(key, value, 'PX', ms).
32
+ this._nodeRedis = typeof client.isOpen === 'boolean';
33
+ this._hits = 0;
34
+ this._misses = 0;
35
+ }
36
+
37
+ /**
38
+ * @param {string} key
39
+ * @returns {Promise<unknown|undefined>}
40
+ */
41
+ async get(key) {
42
+ const raw = await this.client.get(this.prefix + key);
43
+ if (raw === null || raw === undefined) {
44
+ this._misses += 1;
45
+ return undefined;
46
+ }
47
+ this._hits += 1;
48
+ return JSON.parse(raw);
49
+ }
50
+
51
+ /**
52
+ * @param {string} key
53
+ * @param {unknown} value - JSON-serialisable value
54
+ * @param {{ ttlSeconds?: number }} [options]
55
+ */
56
+ async set(key, value, { ttlSeconds = this.ttlSeconds } = {}) {
57
+ const json = JSON.stringify(value);
58
+ if (json === undefined) return;
59
+ const k = this.prefix + key;
60
+ const ms = ttlSeconds > 0 ? Math.round(ttlSeconds * 1000) : 0;
61
+ if (!ms) {
62
+ await this.client.set(k, json);
63
+ } else if (this._nodeRedis) {
64
+ await this.client.set(k, json, { PX: ms });
65
+ } else {
66
+ await this.client.set(k, json, 'PX', ms);
67
+ }
68
+ }
69
+
70
+ /** @param {string} key */
71
+ async delete(key) {
72
+ const del = this.client.del || this.client.unlink;
73
+ if (typeof del === 'function')
74
+ await del.call(this.client, this.prefix + key);
75
+ }
76
+
77
+ /** Hits and misses seen by this instance (size is not tracked). */
78
+ async stats() {
79
+ return { hits: this._hits, misses: this._misses };
80
+ }
81
+ }
82
+
83
+ module.exports = { RedisCache };
@@ -10,7 +10,7 @@ const { detectHallucinations } = require('./hallucination-detector');
10
10
  * @type {object}
11
11
  */
12
12
  const DEFAULT_CONFIG = {
13
- similarityThreshold: 0.3,
13
+ similarityThreshold: 0.5,
14
14
  maxCitationsPerSentence: 3,
15
15
  useIDFWeighting: true,
16
16
  };
@@ -24,7 +24,7 @@ const DEFAULT_CONFIG = {
24
24
  class CitationTracker extends EventEmitter {
25
25
  /**
26
26
  * @param {object} [options] - Override default configuration
27
- * @param {number} [options.similarityThreshold=0.3] - Minimum similarity for a citation match
27
+ * @param {number} [options.similarityThreshold=0.5] - Minimum similarity for a citation match
28
28
  * @param {number} [options.maxCitationsPerSentence=3] - Max citations per sentence
29
29
  * @param {boolean} [options.useIDFWeighting=true] - Whether to apply IDF weighting
30
30
  */
@@ -76,9 +76,11 @@ class CitationTracker extends EventEmitter {
76
76
  'CitationTracker.track(): answer must be a non-empty string',
77
77
  );
78
78
  }
79
- if (!Array.isArray(retrievedDocs) || retrievedDocs.length === 0) {
79
+ // An empty array is valid (retrieval found nothing): every sentence is
80
+ // then reported as unsupported rather than failing the whole run.
81
+ if (!Array.isArray(retrievedDocs)) {
80
82
  throw new Error(
81
- 'CitationTracker.track(): retrievedDocs must be a non-empty array',
83
+ 'CitationTracker.track(): retrievedDocs must be an array',
82
84
  );
83
85
  }
84
86
 
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * @param {Array<{sentence: string, sources: Array<{docId: string|undefined, docIndex: number, score: number}>}>} citationResults
12
12
  * @param {object} [options]
13
- * @param {number} [options.threshold=0.3] - Score threshold for grounded classification
13
+ * @param {number} [options.threshold=0.5] - Score threshold for grounded classification
14
14
  * @returns {{
15
15
  * hallucinationRate: number,
16
16
  * sentences: Array<{text: string, classification: string, bestScore: number, bestSource: object|null}>,
@@ -18,7 +18,7 @@
18
18
  * }}
19
19
  */
20
20
  function detectHallucinations(citationResults, options = {}) {
21
- const { threshold = 0.3 } = options;
21
+ const { threshold = 0.5 } = options;
22
22
 
23
23
  if (!Array.isArray(citationResults) || citationResults.length === 0) {
24
24
  return {
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const { tokenize, computeJaccardSimilarity } = require('../evaluate/scoring');
3
+ const { tokenize, computeSupportScore } = require('../evaluate/scoring');
4
4
 
5
5
  /**
6
6
  * Build IDF (Inverse Document Frequency) weights from a collection of documents.
7
- * IDF = log(totalDocs / docsContainingToken)
7
+ * IDF = log(1 + totalDocs / docsContainingToken). The +1 keeps weights
8
+ * positive, so tokens found in every document (and every token when only one
9
+ * document was retrieved) still count instead of weighing zero.
8
10
  * @param {Array<{content?: string, text?: string}>} docs - Array of document objects
9
11
  * @returns {Map<string, number>} Map of token to IDF weight
10
12
  */
@@ -24,24 +26,25 @@ function buildIDFWeights(docs) {
24
26
 
25
27
  const idfWeights = new Map();
26
28
  for (const [token, df] of docFrequency) {
27
- idfWeights.set(token, Math.log(totalDocs / df));
29
+ idfWeights.set(token, Math.log(1 + totalDocs / df));
28
30
  }
29
31
 
30
32
  return idfWeights;
31
33
  }
32
34
 
33
35
  /**
34
- * Map a sentence to its most relevant source documents using token similarity.
36
+ * Map a sentence to its most relevant source documents by how much of the
37
+ * sentence each document supports (see computeSupportScore).
35
38
  * @param {string} sentence - The sentence to map
36
39
  * @param {Array<{content?: string, text?: string, id?: string}>} docs - Source documents
37
40
  * @param {object} [options] - Configuration options
38
41
  * @param {number} [options.maxCitations=3] - Maximum number of citations to return
39
- * @param {number} [options.threshold=0.3] - Minimum similarity threshold
42
+ * @param {number} [options.threshold=0.5] - Minimum similarity threshold
40
43
  * @param {Map<string, number>|null} [options.idfWeights=null] - Optional IDF weights for weighted similarity
41
44
  * @returns {Array<{docIndex: number, docId: string|undefined, score: number, matchedTokens: string[]}>}
42
45
  */
43
46
  function mapSentenceToSources(sentence, docs, options = {}) {
44
- const { maxCitations = 3, threshold = 0.3, idfWeights = null } = options;
47
+ const { maxCitations = 3, threshold = 0.5, idfWeights = null } = options;
45
48
 
46
49
  const sentenceTokens = tokenize(sentence);
47
50
  if (!sentenceTokens.length) return [];
@@ -54,29 +57,13 @@ function mapSentenceToSources(sentence, docs, options = {}) {
54
57
  const docTokens = tokenize(docText);
55
58
  if (!docTokens.length) continue;
56
59
 
57
- let score;
58
- const sentenceSet = new Set(sentenceTokens);
60
+ // Share of the sentence's content tokens found in this document.
59
61
  const docSet = new Set(docTokens);
60
- const matched = [...sentenceSet].filter((t) => docSet.has(t));
61
-
62
- if (idfWeights) {
63
- // IDF-weighted Jaccard: sum IDF weights of intersection / sum IDF weights of union
64
- const union = new Set([...sentenceSet, ...docSet]);
65
- let intersectionWeight = 0;
66
- let unionWeight = 0;
67
-
68
- for (const token of union) {
69
- const weight = idfWeights.get(token) || 1;
70
- if (sentenceSet.has(token) && docSet.has(token)) {
71
- intersectionWeight += weight;
72
- }
73
- unionWeight += weight;
74
- }
75
-
76
- score = unionWeight > 0 ? intersectionWeight / unionWeight : 0;
77
- } else {
78
- score = computeJaccardSimilarity(sentenceTokens, docTokens);
79
- }
62
+ const score = computeSupportScore(sentenceTokens, docSet, idfWeights, {
63
+ claimText: sentence,
64
+ docText,
65
+ });
66
+ const matched = [...new Set(sentenceTokens)].filter((t) => docSet.has(t));
80
67
 
81
68
  if (score >= threshold) {
82
69
  results.push({
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `rag-pipeline ai ...`: training, benchmarking, relevance feedback and file
5
+ * inspection built on the real pipeline components (.ragrc.json + index).
6
+ */
7
+
8
+ const { Command, Option } = require("commander");
9
+ const { parsePositiveInteger } = require("../shared/cli-utils.js");
10
+ // The handler (document loaders: jsdom, marked, csv-parse) loads when an
11
+ // ai command runs, not when the CLI starts.
12
+ const aiHandler = () => require("../handlers/ai-handler.js"); // eslint-disable-line global-require
13
+
14
+ /**
15
+ * @param {() => object} getGlobalOptions - Returns program.opts()
16
+ * @returns {Command}
17
+ */
18
+ function createAiCommand(getGlobalOptions) {
19
+ const ai = new Command("ai").description(
20
+ "Train the local embedder, benchmark queries, relevance feedback, file inspection",
21
+ );
22
+
23
+ const train = ai
24
+ .command("train")
25
+ .description("Fit corpus-trained components");
26
+ train
27
+ .command("embedding")
28
+ .description(
29
+ "Refit the local TF-IDF embedder (on the index, or on [path]) and re-embed the index",
30
+ )
31
+ .argument(
32
+ "[path]",
33
+ "Train on documents from this file or directory instead",
34
+ )
35
+ .action(async (inputPath) => {
36
+ await aiHandler().handleTrainEmbedding(getGlobalOptions(), inputPath);
37
+ });
38
+
39
+ ai.command("benchmark")
40
+ .description(
41
+ "Measure query latency against the configured pipeline and index",
42
+ )
43
+ .option(
44
+ "--queries <file>",
45
+ "Queries file: JSON array of strings or one per line (default: sampled from the index)",
46
+ )
47
+ .option("--runs <n>", "Passes over the query set", parsePositiveInteger, 3)
48
+ .option("--top-k <n>", "Passages to retrieve", parsePositiveInteger, 3)
49
+ .option("--json", "Print the measurements as JSON")
50
+ .action(async (options) => {
51
+ await aiHandler().handleBenchmark(getGlobalOptions(), options);
52
+ });
53
+
54
+ ai.command("feedback")
55
+ .description(
56
+ "Record whether an indexed chunk was a good result for a question",
57
+ )
58
+ .argument("<question>", "Question the chunk was returned for")
59
+ .argument("<docId>", "Chunk id as shown by `ai search` (e.g. docs/a.md#0)")
60
+ .addOption(
61
+ new Option("--rating <rating>", "Relevance vote")
62
+ .choices(["up", "down"])
63
+ .makeOptionMandatory(),
64
+ )
65
+ .action(async (question, docId, options) => {
66
+ await aiHandler().handleFeedback(
67
+ getGlobalOptions(),
68
+ question,
69
+ docId,
70
+ options,
71
+ );
72
+ });
73
+
74
+ ai.command("search")
75
+ .description(
76
+ "Retrieve passages re-scored with stored feedback (retrieval only)",
77
+ )
78
+ .argument("<question>", "Question text")
79
+ .option("--top-k <n>", "Passages to return", parsePositiveInteger, 3)
80
+ .option("--json", "Print the results as JSON")
81
+ .action(async (question, options) => {
82
+ await aiHandler().handleSearch(getGlobalOptions(), question, options);
83
+ });
84
+
85
+ const multimodal = ai
86
+ .command("multimodal")
87
+ .description("Inspect files before ingesting");
88
+ multimodal
89
+ .command("process")
90
+ .description(
91
+ "Report type, characters and chunks for text files (images need a vision model)",
92
+ )
93
+ .argument("<path>", "File or directory")
94
+ .option("--json", "Print the report as JSON")
95
+ .action(async (inputPath, options) => {
96
+ await aiHandler().handleMultimodalProcess(
97
+ getGlobalOptions(),
98
+ inputPath,
99
+ options,
100
+ );
101
+ });
102
+
103
+ return ai;
104
+ }
105
+
106
+ module.exports = { createAiCommand };
@@ -6,6 +6,12 @@
6
6
  const chalk = require("chalk");
7
7
 
8
8
  function setupEventListeners(hub, certification) {
9
+ // The hub emits 'error' and then throws a descriptive error that each
10
+ // command reports. Without a listener, EventEmitter would throw its own
11
+ // "Unhandled error" first and hide that message.
12
+ hub.on("error", () => {});
13
+ certification.on("error", () => {});
14
+
9
15
  // Hub events
10
16
  hub.on("install_start", (data) => {
11
17
  console.log(
@@ -69,34 +69,6 @@ async function searchPlugins(hub, query, _options) {
69
69
  }
70
70
  }
71
71
 
72
- async function installPlugin(hub, plugin, _options) {
73
- try {
74
- const installOptions = {
75
- securityScan: _options.securityScan,
76
- requireCertified: _options.requireCertified,
77
- sandboxTimeout: _options.sandboxTimeout,
78
- };
79
-
80
- const result = await hub.installPlugin(
81
- plugin,
82
- _options.version,
83
- installOptions,
84
- );
85
-
86
- console.log(chalk.green("\nāœ… Installation completed successfully!")); // eslint-disable-line no-console
87
- console.log(chalk.blue(`Plugin: ${result.pluginInfo.name}`)); // eslint-disable-line no-console
88
- console.log(chalk.blue(`Version: ${result.version}`)); // eslint-disable-line no-console
89
- console.log(chalk.blue(`Install Path: ${result.installPath}`)); // eslint-disable-line no-console
90
-
91
- if (result.pluginInfo.certified) {
92
- console.log(chalk.green("šŸ† This plugin is certified!")); // eslint-disable-line no-console
93
- }
94
- } catch (error) {
95
- console.error(chalk.red(`Installation failed: ${error.message}`)); // eslint-disable-line no-console
96
- process.exit(1);
97
- }
98
- }
99
-
100
72
  async function getPluginInfo(hub, plugin) {
101
73
  const spinner = ora("Fetching plugin information...").start();
102
74
 
@@ -170,50 +142,6 @@ async function getPluginInfo(hub, plugin) {
170
142
  }
171
143
  }
172
144
 
173
- async function listInstalledPlugins(hub, _options) {
174
- const spinner = ora("Loading installed plugins...").start();
175
-
176
- try {
177
- const installed = await hub.getInstalledPlugins();
178
- spinner.stop();
179
-
180
- if (installed.length === 0) {
181
- console.log(chalk.yellow("No plugins installed.")); // eslint-disable-line no-console
182
- return;
183
- }
184
-
185
- if (_options.format === "json") {
186
- console.log(JSON.stringify(installed, null, 2)); // eslint-disable-line no-console
187
- return;
188
- }
189
-
190
- console.log(chalk.green(`${installed.length} plugins installed:\n`)); // eslint-disable-line no-console
191
-
192
- const table = new Table({
193
- head: ["Name", "Version", "Type", "Last Used", "Status"],
194
- colWidths: [25, 12, 15, 15, 10],
195
- });
196
-
197
- for (const plugin of installed) {
198
- const lastUsed = plugin.lastUsed
199
- ? new Date(plugin.lastUsed).toLocaleDateString()
200
- : chalk.gray("Never");
201
-
202
- const status = plugin.certified
203
- ? chalk.green("Certified")
204
- : chalk.gray("Standard");
205
-
206
- table.push([plugin.name, plugin.version, plugin._type, lastUsed, status]);
207
- }
208
-
209
- console.log(table.toString()); // eslint-disable-line no-console
210
- } catch (error) {
211
- spinner.stop();
212
- console.error(chalk.red(`Failed to list plugins: ${error.message}`)); // eslint-disable-line no-console
213
- process.exit(1);
214
- }
215
- }
216
-
217
145
  async function publishPlugin(hub, pluginPath, _options) {
218
146
  if (_options.dryRun) {
219
147
  console.log(
@@ -282,9 +210,7 @@ async function getTrendingPlugins(hub, _options) {
282
210
 
283
211
  module.exports = {
284
212
  searchPlugins,
285
- installPlugin,
286
213
  getPluginInfo,
287
- listInstalledPlugins,
288
214
  publishPlugin,
289
215
  getTrendingPlugins,
290
216
  };