@devilsdev/rag-pipeline-utils 2.4.4 → 2.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.
Files changed (225) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +345 -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 +4992 -1242
  9. package/dist/index.d.ts +80 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  12. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  13. package/dist/types/agentic/query-planner.d.ts +145 -0
  14. package/dist/types/agentic/self-critique.d.ts +45 -0
  15. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  16. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  17. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  18. package/dist/types/ai/multimodal.d.ts +39 -0
  19. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  20. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  21. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  22. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  23. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  24. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  25. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  26. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  27. package/dist/types/citation/citation-tracker.d.ts +64 -0
  28. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  29. package/dist/types/citation/source-mapper.d.ts +37 -0
  30. package/dist/types/connectors/anthropic-connector.d.ts +92 -0
  31. package/dist/types/connectors/base-connector.d.ts +87 -0
  32. package/dist/types/connectors/cohere-connector.d.ts +89 -0
  33. package/dist/types/connectors/connector-utils.d.ts +110 -0
  34. package/dist/types/connectors/local-embedder.d.ts +86 -0
  35. package/dist/types/connectors/memory-retriever.d.ts +91 -0
  36. package/dist/types/connectors/ollama-connector.d.ts +110 -0
  37. package/dist/types/connectors/openai-connector.d.ts +114 -0
  38. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  39. package/dist/types/cost/cost-calculator.d.ts +152 -0
  40. package/dist/types/cost/cost-tracker.d.ts +126 -0
  41. package/dist/types/cost/token-budget.d.ts +110 -0
  42. package/dist/types/dag/core/error-context.d.ts +239 -0
  43. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  44. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  45. package/dist/types/dag/dag-engine.d.ts +156 -0
  46. package/dist/types/dag/dag-node.d.ts +37 -0
  47. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  48. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  49. package/dist/types/debug/execution-tracer.d.ts +57 -0
  50. package/dist/types/debug/trace-inspector.d.ts +31 -0
  51. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  52. package/dist/types/enterprise/data-governance.d.ts +203 -0
  53. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  54. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  55. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  56. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  57. package/dist/types/evaluate/groundedness.d.ts +60 -0
  58. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  59. package/dist/types/evaluate/relevance.d.ts +21 -0
  60. package/dist/types/evaluate/scoring.d.ts +123 -0
  61. package/dist/types/experimental.d.ts +15 -0
  62. package/dist/types/graph/entity-extractor.d.ts +84 -0
  63. package/dist/types/graph/graph-index.d.ts +49 -0
  64. package/dist/types/graph/graph-retriever.d.ts +81 -0
  65. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  66. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  67. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  68. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  69. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  70. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  71. package/dist/types/mcp/mcp-server.d.ts +85 -0
  72. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  73. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  74. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  75. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  76. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  77. package/dist/types/retrieval/file-vector-store.d.ts +87 -0
  78. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  79. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  80. package/dist/types/security/jwt-config.d.ts +80 -0
  81. package/dist/types/security/jwt-validator.d.ts +195 -0
  82. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  83. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  84. package/dist/types/security/validator.d.ts +5 -0
  85. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  86. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  87. package/dist/types/streaming/stream-router.d.ts +88 -0
  88. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  89. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  90. package/dist/types/templates/scaffold.d.ts +55 -0
  91. package/dist/types/templates/template-registry.d.ts +63 -0
  92. package/dist/types/utils/embed-query.d.ts +20 -0
  93. package/dist/types/utils/logger.d.ts +50 -0
  94. package/dist/types/utils/rate-limiter.d.ts +66 -0
  95. package/dist/types/utils/retry.d.ts +27 -0
  96. package/dist/types/utils/run-abort.d.ts +34 -0
  97. package/dist/types/utils/secure-logging.d.ts +51 -0
  98. package/dist/types/utils/structured-logger.d.ts +126 -0
  99. package/package.json +53 -29
  100. package/src/agentic/agentic-pipeline.js +75 -7
  101. package/src/agentic/iterative-retriever.js +47 -10
  102. package/src/agentic/query-planner.js +329 -78
  103. package/src/citation/citation-tracker.js +6 -4
  104. package/src/citation/hallucination-detector.js +2 -2
  105. package/src/citation/source-mapper.js +15 -28
  106. package/src/cli/commands/ai.js +101 -0
  107. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  108. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  109. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  110. package/src/cli/commands/plugin-hub.js +96 -36
  111. package/src/cli/diagnostics/config-checker.js +76 -63
  112. package/src/cli/diagnostics/environment-checker.js +62 -31
  113. package/src/cli/diagnostics/plugin-checker.js +36 -89
  114. package/src/cli/doctor-command.js +39 -5
  115. package/src/cli/enhanced-cli-commands.js +72 -217
  116. package/src/cli/handlers/ai-handler.js +364 -0
  117. package/src/cli/handlers/diagnostics-handler.js +78 -31
  118. package/src/cli/handlers/ingest-handler.js +28 -22
  119. package/src/cli/handlers/init-handler.js +148 -25
  120. package/src/cli/handlers/query-handler.js +41 -26
  121. package/src/cli/handlers/run-handler.js +16 -167
  122. package/src/cli/handlers/serve-handler.js +60 -0
  123. package/src/cli/services/bench-stats.js +54 -0
  124. package/src/cli/services/benchmark.js +161 -0
  125. package/src/cli/services/feedback.js +155 -0
  126. package/src/cli/services/rag-service.js +322 -0
  127. package/src/cli/shared/cli-utils.js +24 -7
  128. package/src/cli/shared/config-context.js +21 -0
  129. package/src/cli/wizard/config-builder.js +81 -0
  130. package/src/cli/wizard/init-wizard.js +617 -0
  131. package/src/config/component-factory.js +125 -0
  132. package/src/config/load-config.js +29 -10
  133. package/src/config/load-plugin-config.js +46 -2
  134. package/src/connectors/anthropic-connector.js +93 -55
  135. package/src/connectors/base-connector.js +74 -19
  136. package/src/connectors/cohere-connector.js +88 -32
  137. package/src/connectors/connector-utils.js +302 -0
  138. package/src/connectors/local-embedder.js +43 -5
  139. package/src/connectors/memory-retriever.js +108 -15
  140. package/src/connectors/ollama-connector.js +83 -49
  141. package/src/connectors/openai-connector.js +108 -44
  142. package/src/connectors/rag-prompt.js +88 -0
  143. package/src/core/create-pipeline.js +209 -60
  144. package/src/core/plugin-registry.js +87 -11
  145. package/src/cost/cost-calculator.js +205 -32
  146. package/src/dev/dev-server.js +166 -11
  147. package/src/dev/hot-reload.js +40 -1
  148. package/src/ecosystem/plugin-checks.js +181 -0
  149. package/src/ecosystem/plugin-hub.js +50 -125
  150. package/src/ecosystem/plugin-installer.js +298 -0
  151. package/src/ecosystem/plugin-probe.js +127 -0
  152. package/src/ecosystem/plugin-scanner.js +185 -0
  153. package/src/ecosystem/tar-extract.js +114 -0
  154. package/src/enterprise/audit-logging.js +158 -29
  155. package/src/enterprise/sso-integration.js +715 -228
  156. package/src/evaluate/context-metrics.js +28 -16
  157. package/src/evaluate/evaluator.js +3 -3
  158. package/src/evaluate/faithfulness.js +18 -14
  159. package/src/evaluate/groundedness.js +43 -12
  160. package/src/evaluate/pipeline-evaluator.js +35 -8
  161. package/src/evaluate/scoring.js +251 -3
  162. package/src/experimental.js +71 -0
  163. package/src/guardrails/guardrails-pipeline.js +69 -7
  164. package/src/guardrails/injection-detector.js +283 -0
  165. package/src/guardrails/post-generation-guard.js +19 -0
  166. package/src/guardrails/pre-retrieval-guard.js +36 -36
  167. package/src/guardrails/retrieval-guard.js +58 -8
  168. package/src/index.js +36 -1
  169. package/src/loader/document-reader.js +91 -0
  170. package/src/reranker/cascade-reranker.js +3 -0
  171. package/src/reranker/embedding-reranker.js +136 -21
  172. package/src/reranker/llm-reranker.js +79 -12
  173. package/src/reranker/scoring-reranker.js +3 -0
  174. package/src/retrieval/bm25-search.js +12 -6
  175. package/src/retrieval/file-vector-store.js +212 -0
  176. package/src/retrieval/hybrid-retriever.js +22 -19
  177. package/src/security/jwt-config.js +13 -2
  178. package/src/security/jwt-validator.js +134 -30
  179. package/src/security/plugin-sandbox.js +18 -1
  180. package/src/security/plugin-signature-verifier.js +245 -70
  181. package/src/utils/deprecate.js +69 -0
  182. package/src/utils/embed-query.js +33 -0
  183. package/src/utils/input-sanitizer.js +19 -4
  184. package/src/utils/logger.js +34 -4
  185. package/src/utils/run-abort.js +102 -0
  186. package/src/ai/federated-learning.js +0 -211
  187. package/src/ai/federation.js +0 -71
  188. package/src/ai/index.js +0 -22
  189. package/src/ai/model-trainer.js +0 -217
  190. package/src/ai/model-training.js +0 -202
  191. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  192. package/src/ai/orchestrator.js +0 -65
  193. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  194. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  195. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  196. package/src/cli/commands/ai-ml/index.js +0 -17
  197. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  198. package/src/cli/commands/ai-ml/shared.js +0 -51
  199. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  200. package/src/cli/commands/ai-ml.js +0 -386
  201. package/src/cli/commands/docs.js +0 -228
  202. package/src/cli/commands/dx.js +0 -538
  203. package/src/cli/commands/wizard.js +0 -627
  204. package/src/cli/input-sanitization-middleware.js +0 -356
  205. package/src/cli/interactive-wizard.js +0 -373
  206. package/src/cli/templates/development.template.json +0 -64
  207. package/src/cli/templates/minimal.template.json +0 -33
  208. package/src/cli/templates/production.template.json +0 -104
  209. package/src/cli/templates/testing.template.json +0 -45
  210. package/src/cli/wizard/configuration.js +0 -148
  211. package/src/cli/wizard/observability.js +0 -79
  212. package/src/cli/wizard/performance.js +0 -76
  213. package/src/cli/wizard/plugin-selection.js +0 -297
  214. package/src/cli/wizard/preview-save.js +0 -172
  215. package/src/cli/wizard/project-setup.js +0 -66
  216. package/src/core/pipeline-factory.js +0 -59
  217. package/src/dag/enhanced-dag-engine.js +0 -894
  218. package/src/dx/index.js +0 -40
  219. package/src/dx/integration-templates.js +0 -662
  220. package/src/dx/performance-profiler.js +0 -968
  221. package/src/dx/realtime-debugger.js +0 -755
  222. package/src/dx/visual-pipeline-builder.js +0 -578
  223. package/src/ingest.js +0 -59
  224. package/src/pipeline/pipeline.js +0 -18
  225. package/src/query.js +0 -117
@@ -1,10 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const {
4
- tokenize,
5
- splitSentences,
6
- computeJaccardSimilarity,
7
- } = require('./scoring');
3
+ const { tokenize, splitSentences, computeSupportScore } = require('./scoring');
8
4
 
9
5
  /**
10
6
  * Compute context precision: the fraction of retrieved documents that are relevant to the query.
@@ -12,7 +8,7 @@ const {
12
8
  * @param {string} query - The user query
13
9
  * @param {Array<{content: string}>} retrievedDocs - Array of retrieved documents
14
10
  * @param {object} [options={}] - Configuration options
15
- * @param {number} [options.threshold=0.2] - Minimum Jaccard similarity for a doc to be considered relevant
11
+ * @param {number} [options.threshold=0.2] - Minimum share of query content terms a doc must contain to be considered relevant
16
12
  * @returns {{ score: number, relevantDocs: number, totalDocs: number, perDocScores: Array<{ index: number, score: number, isRelevant: boolean }> }}
17
13
  */
18
14
  function computeContextPrecision(query, retrievedDocs, options = {}) {
@@ -39,7 +35,8 @@ function computeContextPrecision(query, retrievedDocs, options = {}) {
39
35
  }
40
36
 
41
37
  const docTokens = tokenize(docContent);
42
- const score = computeJaccardSimilarity(queryTokens, docTokens);
38
+ // Share of the query's content terms that the document contains.
39
+ const score = computeSupportScore(queryTokens, docTokens);
43
40
  perDocScores.push({ index: i, score, isRelevant: score >= threshold });
44
41
  }
45
42
 
@@ -51,22 +48,34 @@ function computeContextPrecision(query, retrievedDocs, options = {}) {
51
48
  }
52
49
 
53
50
  /**
54
- * Compute context recall: the fraction of answer claims that are supported by retrieved documents.
51
+ * Compute context recall: the fraction of the claims in a REFERENCE
52
+ * (ground-truth) answer that the retrieved documents support. It measures
53
+ * whether retrieval found everything needed to answer correctly; pass the
54
+ * expected answer, not the generated one (scoring the generated answer
55
+ * against the documents is faithfulness).
55
56
  *
56
- * @param {string} answer - The generated answer text
57
+ * A claim is one sentence of the reference answer. It is supported when some
58
+ * document scores at least `threshold` with computeSupportScore, including
59
+ * the numeric consistency check.
60
+ *
61
+ * @param {string} referenceAnswer - The reference (ground-truth) answer text
57
62
  * @param {Array<{content: string}>} retrievedDocs - Array of retrieved documents
58
63
  * @param {object} [options={}] - Configuration options
59
- * @param {number} [options.threshold=0.3] - Minimum Jaccard similarity for a claim to be considered supported
64
+ * @param {number} [options.threshold=0.5] - Minimum share of a claim's content terms a doc must contain to support it
60
65
  * @returns {{ score: number, supportedClaims: number, totalClaims: number, details: Array<{ claim: string, score: number, isSupported: boolean, bestDocIndex: number }> }}
61
66
  */
62
- function computeContextRecall(answer, retrievedDocs, options = {}) {
63
- const { threshold = 0.3 } = options;
64
-
65
- if (!answer || typeof answer !== 'string' || answer.trim().length === 0) {
67
+ function computeContextRecall(referenceAnswer, retrievedDocs, options = {}) {
68
+ const { threshold = 0.5 } = options;
69
+
70
+ if (
71
+ !referenceAnswer ||
72
+ typeof referenceAnswer !== 'string' ||
73
+ referenceAnswer.trim().length === 0
74
+ ) {
66
75
  return { score: 0, supportedClaims: 0, totalClaims: 0, details: [] };
67
76
  }
68
77
 
69
- const claims = splitSentences(answer);
78
+ const claims = splitSentences(referenceAnswer);
70
79
 
71
80
  if (claims.length === 0) {
72
81
  return { score: 0, supportedClaims: 0, totalClaims: 0, details: [] };
@@ -101,7 +110,10 @@ function computeContextRecall(answer, retrievedDocs, options = {}) {
101
110
  }
102
111
 
103
112
  const docTokens = tokenize(docContent);
104
- const similarity = computeJaccardSimilarity(claimTokens, docTokens);
113
+ const similarity = computeSupportScore(claimTokens, docTokens, null, {
114
+ claimText: claim,
115
+ docText: docContent,
116
+ });
105
117
 
106
118
  if (similarity > bestScore) {
107
119
  bestScore = similarity;
@@ -22,15 +22,15 @@ async function evaluateRagDataset(_datasetPath, _config) {
22
22
  const results = [];
23
23
  for (const { prompt, expected } of cases) {
24
24
  try {
25
- logger.info({ prompt }, 'Evaluating case');
25
+ logger.info('Evaluating case', { prompt });
26
26
  const result = await pipeline.run({ query: prompt });
27
- const actual = result.results || '';
27
+ const actual = typeof result.answer === 'string' ? result.answer : '';
28
28
  const success =
29
29
  result.success && normalizeText(actual) === normalizeText(expected);
30
30
  const scores = scoreAnswer(actual, expected);
31
31
  results.push({ prompt, expected, actual, success, scores });
32
32
  } catch (err) {
33
- logger.error({ prompt, error: err.message }, 'Evaluation failed');
33
+ logger.error('Evaluation failed', { prompt, error: err.message });
34
34
  results.push({
35
35
  prompt,
36
36
  expected,
@@ -1,10 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const {
4
- splitSentences,
5
- tokenize,
6
- computeJaccardSimilarity,
7
- } = require('./scoring');
3
+ const { splitSentences, tokenize, computeSupportScore } = require('./scoring');
8
4
  const { mapSentenceToSources } = require('../citation/source-mapper');
9
5
 
10
6
  /**
@@ -14,11 +10,11 @@ const { mapSentenceToSources } = require('../citation/source-mapper');
14
10
  * @param {string} answer - The generated answer text
15
11
  * @param {Array<{content: string}>} retrievedDocs - Array of retrieved documents with content
16
12
  * @param {object} [options={}] - Configuration options
17
- * @param {number} [options.threshold=0.3] - Minimum similarity score to consider a sentence faithful
13
+ * @param {number} [options.threshold=0.5] - Minimum similarity score to consider a sentence faithful
18
14
  * @returns {{ score: number, faithfulSentences: number, totalSentences: number, details: Array<{ sentence: string, score: number, isFaithful: boolean, bestSource: number }> }}
19
15
  */
20
16
  function computeFaithfulness(answer, retrievedDocs, options = {}) {
21
- const { threshold = 0.3 } = options;
17
+ const { threshold = 0.5 } = options;
22
18
 
23
19
  if (!answer || typeof answer !== 'string' || answer.trim().length === 0) {
24
20
  return { score: 0, faithfulSentences: 0, totalSentences: 0, details: [] };
@@ -60,7 +56,10 @@ function computeFaithfulness(answer, retrievedDocs, options = {}) {
60
56
  }
61
57
 
62
58
  const docTokens = tokenize(docContent);
63
- const similarity = computeJaccardSimilarity(sentenceTokens, docTokens);
59
+ const similarity = computeSupportScore(sentenceTokens, docTokens, null, {
60
+ claimText: sentence,
61
+ docText: docContent,
62
+ });
64
63
 
65
64
  if (similarity > bestScore) {
66
65
  bestScore = similarity;
@@ -102,13 +101,18 @@ function computeFaithfulnessFromCitations(citationResult) {
102
101
  ? citationResult.groundednessScore
103
102
  : 0;
104
103
 
104
+ // CitationTracker only lists sources at or above its similarity threshold,
105
+ // so a sentence is faithful exactly when it has at least one source.
105
106
  const details = Array.isArray(citationResult.citations)
106
- ? citationResult.citations.map((c) => ({
107
- sentence: c.sentence || c.text || '',
108
- score: c.score || c.similarity || 0,
109
- isFaithful: (c.score || c.similarity || 0) > 0,
110
- bestSource: c.sourceIndex != null ? c.sourceIndex : -1,
111
- }))
107
+ ? citationResult.citations.map((c) => {
108
+ const best = Array.isArray(c.sources) ? c.sources[0] : undefined;
109
+ return {
110
+ sentence: c.sentence || c.text || '',
111
+ score: best ? best.score : 0,
112
+ isFaithful: Boolean(best),
113
+ bestSource: best ? best.docIndex : -1,
114
+ };
115
+ })
112
116
  : [];
113
117
 
114
118
  return { score, details };
@@ -16,20 +16,40 @@ const DEFAULT_WEIGHTS = {
16
16
  contextRecall: 0.3,
17
17
  };
18
18
 
19
+ /** Reason reported when context recall cannot be computed. */
20
+ const NO_REFERENCE_REASON =
21
+ 'No reference answer provided: context recall measures how much of a ' +
22
+ 'reference (ground-truth) answer the retrieved documents support.';
23
+
19
24
  /**
20
- * Compute a composite groundedness score combining faithfulness, context precision, and context recall.
21
- * Uses a weighted average of the three component metrics.
25
+ * Compute a composite groundedness score from faithfulness, context precision
26
+ * and (when a reference answer is given) context recall.
27
+ *
28
+ * Formula: a weighted mean over the components that are available,
29
+ *
30
+ * score = sum(w_i * m_i) / sum(w_i)
31
+ *
32
+ * With a reference answer and the default weights this is
33
+ * 0.4 * faithfulness + 0.3 * contextPrecision + 0.3 * contextRecall.
34
+ * Without one, context recall is left out and the remaining weights are
35
+ * renormalised:
36
+ * (0.4 * faithfulness + 0.3 * contextPrecision) / 0.7.
37
+ * If every available weight is 0 the score is 0.
22
38
  *
23
39
  * @param {string} query - The user query
24
40
  * @param {string} answer - The generated answer text
25
41
  * @param {Array<{content: string}>} retrievedDocs - Array of retrieved documents
26
42
  * @param {object} [options={}] - Configuration options
27
43
  * @param {{ faithfulness: number, contextPrecision: number, contextRecall: number }} [options.weights] - Custom weights for each component
28
- * @param {number} [options.threshold=0.3] - Similarity threshold passed to component metrics
44
+ * @param {number} [options.threshold=0.5] - Similarity threshold passed to component metrics
45
+ * @param {string} [options.referenceAnswer] - Reference (ground-truth) answer used for context recall
29
46
  * @returns {{ score: number, components: { faithfulness: object, contextPrecision: object, contextRecall: object }, weights: { faithfulness: number, contextPrecision: number, contextRecall: number } }}
47
+ * `components.contextRecall` is `{ score: null, skipped: true, reason }`
48
+ * when no reference answer was given.
30
49
  */
31
50
  function computeGroundedness(query, answer, retrievedDocs, options = {}) {
32
- const { weights = DEFAULT_WEIGHTS, threshold = 0.3 } = options;
51
+ const { threshold = 0.5, referenceAnswer } = options;
52
+ const weights = { ...DEFAULT_WEIGHTS, ...(options.weights || {}) };
33
53
 
34
54
  const faithfulness = computeFaithfulness(answer, retrievedDocs, {
35
55
  threshold,
@@ -37,14 +57,25 @@ function computeGroundedness(query, answer, retrievedDocs, options = {}) {
37
57
  const contextPrecision = computeContextPrecision(query, retrievedDocs, {
38
58
  threshold: threshold * 0.67,
39
59
  });
40
- const contextRecall = computeContextRecall(answer, retrievedDocs, {
41
- threshold,
42
- });
60
+ const hasReference =
61
+ typeof referenceAnswer === 'string' && referenceAnswer.trim().length > 0;
62
+ const contextRecall = hasReference
63
+ ? computeContextRecall(referenceAnswer, retrievedDocs, { threshold })
64
+ : { score: null, skipped: true, reason: NO_REFERENCE_REASON };
65
+
66
+ const parts = [
67
+ [weights.faithfulness, faithfulness.score],
68
+ [weights.contextPrecision, contextPrecision.score],
69
+ ];
70
+ if (hasReference) parts.push([weights.contextRecall, contextRecall.score]);
43
71
 
44
- const score =
45
- weights.faithfulness * faithfulness.score +
46
- weights.contextPrecision * contextPrecision.score +
47
- weights.contextRecall * contextRecall.score;
72
+ let weighted = 0;
73
+ let totalWeight = 0;
74
+ for (const [w, m] of parts) {
75
+ weighted += w * m;
76
+ totalWeight += w;
77
+ }
78
+ const score = totalWeight > 0 ? weighted / totalWeight : 0;
48
79
 
49
80
  return {
50
81
  score,
@@ -57,4 +88,4 @@ function computeGroundedness(query, answer, retrievedDocs, options = {}) {
57
88
  };
58
89
  }
59
90
 
60
- module.exports = { computeGroundedness, DEFAULT_WEIGHTS };
91
+ module.exports = { computeGroundedness, DEFAULT_WEIGHTS, NO_REFERENCE_REASON };
@@ -10,7 +10,7 @@ const {
10
10
  computeContextPrecision,
11
11
  computeContextRecall,
12
12
  } = require('./context-metrics');
13
- const { computeGroundedness } = require('./groundedness');
13
+ const { computeGroundedness, NO_REFERENCE_REASON } = require('./groundedness');
14
14
  const { scoreAnswer } = require('./scoring');
15
15
 
16
16
  /**
@@ -25,7 +25,7 @@ const DEFAULT_CONFIG = {
25
25
  'contextRecall',
26
26
  'groundedness',
27
27
  ],
28
- threshold: 0.3,
28
+ threshold: 0.5,
29
29
  includeBLEU: false,
30
30
  includeROUGE: false,
31
31
  };
@@ -64,8 +64,14 @@ class PipelineEvaluator extends EventEmitter {
64
64
  * @param {Array<{content: string}>} pipelineResult.results - Retrieved documents
65
65
  * @param {object} [options={}] - Additional evaluation options
66
66
  * @param {object} [options.citationResult] - Pre-computed citation result for faithfulness shortcut
67
- * @param {string} [options.referenceAnswer] - Reference answer for BLEU/ROUGE scoring
68
- * @returns {{ scores: Object<string, number>, details: Object<string, object>, metadata: { metricsComputed: string[], threshold: number, timestamp: string } }}
67
+ * @param {string} [options.referenceAnswer] - Reference (ground-truth)
68
+ * answer. Required for context recall (how much of it the retrieved
69
+ * documents support) and used for BLEU/ROUGE. Without it, contextRecall is
70
+ * left out of `scores`, `details.contextRecall` is
71
+ * `{ score: null, skipped: true, reason }`, it is listed in
72
+ * `metadata.metricsSkipped`, and groundedness renormalises over
73
+ * faithfulness and context precision.
74
+ * @returns {{ scores: Object<string, number>, details: Object<string, object>, metadata: { metricsComputed: string[], metricsSkipped: Array<{metric: string, reason: string}>, threshold: number, timestamp: string } }}
69
75
  */
70
76
  evaluate(pipelineResult, options = {}) {
71
77
  const { citationResult, referenceAnswer } = options;
@@ -76,6 +82,7 @@ class PipelineEvaluator extends EventEmitter {
76
82
  const scores = {};
77
83
  const details = {};
78
84
  const metricsComputed = [];
85
+ const metricsSkipped = [];
79
86
  const { metrics, threshold } = this.config;
80
87
 
81
88
  if (metrics.includes('faithfulness')) {
@@ -106,16 +113,35 @@ class PipelineEvaluator extends EventEmitter {
106
113
  metricsComputed.push('contextPrecision');
107
114
  }
108
115
 
116
+ const hasReference =
117
+ typeof referenceAnswer === 'string' && referenceAnswer.trim().length > 0;
118
+
109
119
  if (metrics.includes('contextRecall')) {
110
- const recallResult = computeContextRecall(answer, results, { threshold });
111
- scores.contextRecall = recallResult.score;
112
- details.contextRecall = recallResult;
113
- metricsComputed.push('contextRecall');
120
+ if (hasReference) {
121
+ const recallResult = computeContextRecall(referenceAnswer, results, {
122
+ threshold,
123
+ });
124
+ scores.contextRecall = recallResult.score;
125
+ details.contextRecall = recallResult;
126
+ metricsComputed.push('contextRecall');
127
+ } else {
128
+ // No ground truth: report why instead of a made-up score.
129
+ details.contextRecall = {
130
+ score: null,
131
+ skipped: true,
132
+ reason: NO_REFERENCE_REASON,
133
+ };
134
+ metricsSkipped.push({
135
+ metric: 'contextRecall',
136
+ reason: NO_REFERENCE_REASON,
137
+ });
138
+ }
114
139
  }
115
140
 
116
141
  if (metrics.includes('groundedness')) {
117
142
  const groundednessResult = computeGroundedness(query, answer, results, {
118
143
  threshold,
144
+ referenceAnswer: hasReference ? referenceAnswer : undefined,
119
145
  });
120
146
  scores.groundedness = groundednessResult.score;
121
147
  details.groundedness = groundednessResult;
@@ -142,6 +168,7 @@ class PipelineEvaluator extends EventEmitter {
142
168
  details,
143
169
  metadata: {
144
170
  metricsComputed,
171
+ metricsSkipped,
145
172
  threshold,
146
173
  timestamp: new Date().toISOString(),
147
174
  },
@@ -11,10 +11,14 @@
11
11
  * @returns {string[]}
12
12
  */
13
13
  function tokenize(text) {
14
- return text
14
+ // Unicode-aware: keep letters/digits in any script, drop apostrophes so
15
+ // "don't" stays one token, and treat every other character (including
16
+ // newlines and hyphens) as a separator.
17
+ return String(text)
15
18
  .toLowerCase()
16
- .replace(/[^a-z0-9 ]/g, '')
17
- .split(/\s+/)
19
+ .replace(/['’]/g, '')
20
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
21
+ .split(' ')
18
22
  .filter(Boolean);
19
23
  }
20
24
 
@@ -108,7 +112,251 @@ function computeJaccardSimilarity(tokensA, tokensB) {
108
112
  return intersection.size / union.size;
109
113
  }
110
114
 
115
+ /**
116
+ * Common English function words. Ignored when measuring how much of a
117
+ * sentence a document supports, so "the" and "is" cannot ground a claim.
118
+ */
119
+ const STOPWORDS = new Set(
120
+ (
121
+ 'a an and are as at be been but by can could did do does for from had has ' +
122
+ 'have he her his how i if in into is it its may me might must my no not ' +
123
+ 'of on or our she should so than that the their them then there these ' +
124
+ 'they this those to was we were what when where which while who why will ' +
125
+ 'with would you your'
126
+ ).split(' '),
127
+ );
128
+
129
+ /**
130
+ * Content tokens of a sentence: tokens minus stopwords. Falls back to all
131
+ * tokens when nothing is left (e.g. "Yes."), so short sentences are still
132
+ * scored rather than skipped.
133
+ *
134
+ * @param {string[]} tokens
135
+ * @returns {string[]}
136
+ */
137
+ function contentTokens(tokens) {
138
+ const content = tokens.filter((t) => !STOPWORDS.has(t));
139
+ return content.length > 0 ? content : tokens;
140
+ }
141
+
142
+ /** Values of English number words. "one" is left out on purpose: it is far
143
+ * more often a pronoun ("no one", "one of") than a quantity. */
144
+ const NUMBER_WORDS = {
145
+ zero: 0,
146
+ two: 2,
147
+ three: 3,
148
+ four: 4,
149
+ five: 5,
150
+ six: 6,
151
+ seven: 7,
152
+ eight: 8,
153
+ nine: 9,
154
+ ten: 10,
155
+ eleven: 11,
156
+ twelve: 12,
157
+ thirteen: 13,
158
+ fourteen: 14,
159
+ fifteen: 15,
160
+ sixteen: 16,
161
+ seventeen: 17,
162
+ eighteen: 18,
163
+ nineteen: 19,
164
+ twenty: 20,
165
+ thirty: 30,
166
+ forty: 40,
167
+ fifty: 50,
168
+ sixty: 60,
169
+ seventy: 70,
170
+ eighty: 80,
171
+ ninety: 90,
172
+ };
173
+ /** Multipliers; "one" counts as 1 only inside a run ("one hundred"). */
174
+ const NUMBER_SCALES = {
175
+ hundred: 100,
176
+ thousand: 1000,
177
+ million: 1e6,
178
+ billion: 1e9,
179
+ trillion: 1e12,
180
+ };
181
+
182
+ /**
183
+ * Digit numbers, optionally with thousands separators or a decimal part (a
184
+ * trailing "%" is ignored). The lookbehind skips digits glued to letters (BM25).
185
+ */
186
+ const DIGIT_NUMBER_RE = /(?<![\p{L}\p{N}])\d+(?:[.,]\d+)*/gu;
187
+
188
+ /**
189
+ * Turn one run of number words into its value: "twenty five" -> 25,
190
+ * "two hundred" -> 200, "three thousand four hundred" -> 3400.
191
+ *
192
+ * @param {string[]} words
193
+ * @returns {number}
194
+ */
195
+ function parseNumberWords(words) {
196
+ let total = 0;
197
+ let current = 0;
198
+ for (const w of words) {
199
+ if (w === 'one') current += 1;
200
+ else if (w in NUMBER_WORDS) current += NUMBER_WORDS[w];
201
+ else if (w === 'hundred') current = (current || 1) * 100;
202
+ else {
203
+ total += (current || 1) * NUMBER_SCALES[w];
204
+ current = 0;
205
+ }
206
+ }
207
+ return total + current;
208
+ }
209
+
210
+ /**
211
+ * Extract the numbers stated in a text, as normalised value strings, so
212
+ * "20", "20%", "twenty" and "Twenty" all yield "20", "1,000" yields "1000"
213
+ * and "2.5" yields "2.5".
214
+ *
215
+ * @param {string} text
216
+ * @returns {{ values: string[], parts: string[] }} values: one entry per
217
+ * number as written; parts: values plus each number word on its own
218
+ * (used on the document side so "twenty-five" also supports "20" and "5"
219
+ * is not required)
220
+ */
221
+ function extractNumbers(text) {
222
+ const values = [];
223
+ const parts = [];
224
+ if (!text || typeof text !== 'string') return { values, parts };
225
+
226
+ for (const match of text.matchAll(DIGIT_NUMBER_RE)) {
227
+ const raw = match[0];
228
+ const pieces = /^\d{1,3}(,\d{3})+(\.\d+)?$/.test(raw)
229
+ ? [raw.replace(/,/g, '')]
230
+ : raw.split(',');
231
+ for (const piece of pieces) {
232
+ const n = Number(piece);
233
+ if (Number.isFinite(n)) values.push(String(n));
234
+ }
235
+ }
236
+
237
+ const words = text.toLowerCase().split(/[^\p{L}]+/u);
238
+ let run = [];
239
+ const flush = () => {
240
+ // "and" only joins a run ("two hundred and five"); never ends one.
241
+ while (run.length && run[run.length - 1] === 'and') run.pop();
242
+ // A lone "one" is not treated as a number (see NUMBER_WORDS).
243
+ if (run.length && !(run.length === 1 && run[0] === 'one')) {
244
+ const numberWords = run.filter((w) => w !== 'and');
245
+ values.push(String(parseNumberWords(numberWords)));
246
+ for (const w of numberWords) {
247
+ if (w in NUMBER_WORDS) parts.push(String(NUMBER_WORDS[w]));
248
+ }
249
+ }
250
+ run = [];
251
+ };
252
+ for (const w of words) {
253
+ const last = run[run.length - 1];
254
+ const isNumberWord =
255
+ w in NUMBER_WORDS ||
256
+ w in NUMBER_SCALES ||
257
+ w === 'one' ||
258
+ (w === 'and' && last in NUMBER_SCALES);
259
+ if (isNumberWord) {
260
+ run.push(w);
261
+ } else {
262
+ flush();
263
+ }
264
+ }
265
+ flush();
266
+
267
+ return { values, parts: [...values, ...parts] };
268
+ }
269
+
270
+ /**
271
+ * Numeric consistency between a claim and a document: every number the claim
272
+ * states (digits, decimals, percentages or number words) must also appear in
273
+ * the document. Lexical overlap alone would ground "thirty days" against a
274
+ * source that says "twenty days".
275
+ *
276
+ * @param {string} claimText
277
+ * @param {string} docText
278
+ * @returns {{ consistent: boolean, claimNumbers: string[], missing: string[] }}
279
+ */
280
+ function checkNumericConsistency(claimText, docText) {
281
+ const claimNumbers = [...new Set(extractNumbers(claimText).values)];
282
+ if (claimNumbers.length === 0) {
283
+ return { consistent: true, claimNumbers, missing: [] };
284
+ }
285
+ const docNumbers = new Set(extractNumbers(docText).parts);
286
+ const missing = claimNumbers.filter((n) => !docNumbers.has(n));
287
+ return { consistent: missing.length === 0, claimNumbers, missing };
288
+ }
289
+
290
+ /**
291
+ * Multiplier applied to the support score when a claim states numbers the
292
+ * document does not contain. Together with the share of matching numbers it
293
+ * keeps the score below 0.4, i.e. under the default 0.5 support threshold.
294
+ */
295
+ const NUMERIC_MISMATCH_FACTOR = 0.4;
296
+
297
+ /**
298
+ * How much of a claim is supported by a document: the (optionally
299
+ * IDF-weighted) fraction of the claim's unique content tokens that appear in
300
+ * the document. Unlike Jaccard against the whole document, this does not
301
+ * shrink as the document gets longer, so a sentence copied verbatim from a
302
+ * long chunk scores 1.
303
+ *
304
+ * Numeric consistency: when `options.claimText` and `options.docText` are
305
+ * given and the claim states a number the document does not contain (see
306
+ * checkNumericConsistency), the score becomes
307
+ * lexicalScore * NUMERIC_MISMATCH_FACTOR (0.4) * matchedNumbers / claimNumbers
308
+ * which is always below 0.4, so the claim fails the default 0.5 threshold.
309
+ *
310
+ * @param {string[]} claimTokens - Tokens of the sentence/claim/query
311
+ * @param {string[]|Set<string>} docTokens - Tokens of the document
312
+ * @param {Map<string, number>|null} [idfWeights] - Optional token weights
313
+ * @param {{ claimText?: string, docText?: string }} [options] - Raw texts
314
+ * enabling the numeric consistency check
315
+ * @returns {number} Support score between 0 and 1
316
+ */
317
+ function computeSupportScore(
318
+ claimTokens,
319
+ docTokens,
320
+ idfWeights = null,
321
+ options = {},
322
+ ) {
323
+ const claim = [...new Set(contentTokens(claimTokens))];
324
+ if (claim.length === 0) return 0;
325
+ const docSet = docTokens instanceof Set ? docTokens : new Set(docTokens);
326
+
327
+ let matched = 0;
328
+ let total = 0;
329
+ for (const token of claim) {
330
+ const weight = idfWeights ? (idfWeights.get(token) ?? 1) : 1;
331
+ total += weight;
332
+ if (docSet.has(token)) matched += weight;
333
+ }
334
+ let score = total > 0 ? matched / total : 0;
335
+
336
+ const { claimText, docText } = options || {};
337
+ if (
338
+ score > 0 &&
339
+ typeof claimText === 'string' &&
340
+ typeof docText === 'string'
341
+ ) {
342
+ const numeric = checkNumericConsistency(claimText, docText);
343
+ if (!numeric.consistent) {
344
+ const share =
345
+ (numeric.claimNumbers.length - numeric.missing.length) /
346
+ numeric.claimNumbers.length;
347
+ score *= NUMERIC_MISMATCH_FACTOR * share;
348
+ }
349
+ }
350
+ return score;
351
+ }
352
+
111
353
  module.exports = {
354
+ STOPWORDS,
355
+ contentTokens,
356
+ computeSupportScore,
357
+ checkNumericConsistency,
358
+ extractNumbers,
359
+ NUMERIC_MISMATCH_FACTOR,
112
360
  scoreAnswer,
113
361
  tokenize,
114
362
  computeBLEU,
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @devilsdev/rag-pipeline-utils/experimental
5
+ *
6
+ * Modules that are simulated, incomplete or not safe for production. They
7
+ * are kept for experimentation and are NOT covered by the semver guarantees
8
+ * in SEMVER.md. See docs-site/docs/Experimental.md for what each one really
9
+ * does. Do not use them for authentication, tenant isolation, compliance or
10
+ * plugin isolation.
11
+ *
12
+ * - Model training / federated learning: metrics and weights are random.
13
+ * - MultiModalProcessor: returns random embeddings; only stores content.
14
+ * - AdaptiveRetrievalEngine: in-memory placeholder strategy.
15
+ * - SSO providers: OAuth2/OIDC use PKCE and OIDC validates id_tokens, but
16
+ * the flows are lightly tested; SAML refuses to authenticate (no XML
17
+ * signature verification); Active Directory is a development stub. A
18
+ * development bypass (any code accepted, fixed identity) exists only
19
+ * with `devBypass: true` outside production.
20
+ * - TenantManager / ResourceMonitor: usage figures are random.
21
+ * - DataGovernanceManager: random classification; erasure deletes nothing.
22
+ * - PluginSandbox: execute() is not functional; plugins are not isolated.
23
+ * - PluginCertification: automated checks return fixed scores (use
24
+ * `rag-pipeline hub certify check` for computed scores).
25
+ */
26
+
27
+ const { MultiModalProcessor } = require('./ai/multimodal');
28
+ const { AdaptiveRetrievalEngine } = require('./ai/retrieval-engine');
29
+ const {
30
+ FederatedLearningCoordinator,
31
+ } = require('./ai/federation/federated-learning-coordinator');
32
+ const {
33
+ ModelTrainingOrchestrator,
34
+ } = require('./ai/training/model-training-orchestrator');
35
+ const { DataGovernanceManager } = require('./enterprise/data-governance');
36
+ const {
37
+ TenantManager,
38
+ ResourceMonitor,
39
+ } = require('./enterprise/multi-tenancy');
40
+ const {
41
+ SSOManager,
42
+ SAMLProvider,
43
+ OAuth2Provider,
44
+ ActiveDirectoryProvider,
45
+ OIDCProvider,
46
+ } = require('./enterprise/sso-integration');
47
+ const { PluginSandbox } = require('./security/plugin-sandbox');
48
+ const { PluginCertification } = require('./ecosystem/plugin-certification');
49
+
50
+ module.exports = {
51
+ // AI/ML
52
+ MultiModalProcessor,
53
+ AdaptiveRetrievalEngine,
54
+ FederatedLearningCoordinator,
55
+ ModelTrainingOrchestrator,
56
+
57
+ // Enterprise
58
+ DataGovernanceManager,
59
+ DataGovernance: DataGovernanceManager,
60
+ TenantManager,
61
+ ResourceMonitor,
62
+ SSOManager,
63
+ SAMLProvider,
64
+ OAuth2Provider,
65
+ ActiveDirectoryProvider,
66
+ OIDCProvider,
67
+
68
+ // Security / ecosystem
69
+ PluginSandbox,
70
+ PluginCertification,
71
+ };