@devilsdev/rag-pipeline-utils 2.5.0 → 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 (66) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/dist/index.cjs +1484 -147
  4. package/dist/index.d.ts +49 -0
  5. package/dist/index.mjs +1 -1
  6. package/dist/testing.cjs +2 -0
  7. package/dist/testing.d.ts +1 -0
  8. package/dist/testing.mjs +5 -0
  9. package/dist/types/adapters/ai-sdk.d.ts +53 -0
  10. package/dist/types/cache/component-keys.d.ts +7 -0
  11. package/dist/types/cache/memory-cache.d.ts +54 -0
  12. package/dist/types/cache/pipeline-cache.d.ts +66 -0
  13. package/dist/types/cache/redis-cache.d.ts +46 -0
  14. package/dist/types/connectors/anthropic-connector.d.ts +7 -2
  15. package/dist/types/connectors/cohere-connector.d.ts +7 -2
  16. package/dist/types/connectors/local-embedder.d.ts +9 -0
  17. package/dist/types/connectors/memory-retriever.d.ts +8 -0
  18. package/dist/types/connectors/ollama-connector.d.ts +7 -2
  19. package/dist/types/connectors/openai-connector.d.ts +16 -1
  20. package/dist/types/core/create-pipeline.d.ts +8 -0
  21. package/dist/types/core/plugin-registry.d.ts +136 -0
  22. package/dist/types/core/stage-contracts.d.ts +36 -0
  23. package/dist/types/core/validate-plugin.d.ts +45 -0
  24. package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
  25. package/dist/types/grounding/check-answer.d.ts +101 -0
  26. package/dist/types/retrieval/file-vector-store.d.ts +12 -0
  27. package/dist/types/testing.d.ts +168 -0
  28. package/package.json +22 -7
  29. package/src/adapters/ai-sdk.js +215 -0
  30. package/src/cache/component-keys.js +38 -0
  31. package/src/cache/memory-cache.js +87 -0
  32. package/src/cache/pipeline-cache.js +260 -0
  33. package/src/cache/redis-cache.js +83 -0
  34. package/src/cli/commands/ai.js +17 -12
  35. package/src/cli/commands/plugin-hub.js +18 -13
  36. package/src/cli/enhanced-cli-commands.js +45 -29
  37. package/src/cli/handlers/query-handler.js +174 -11
  38. package/src/cli/services/instrument.js +83 -0
  39. package/src/cli/services/rag-service.js +51 -12
  40. package/src/cli/wizard/init-wizard.js +2 -17
  41. package/src/config/enhanced-ragrc-schema.js +0 -2
  42. package/src/config/validate-schema.js +4 -1
  43. package/src/connectors/anthropic-connector.js +22 -2
  44. package/src/connectors/cohere-connector.js +20 -2
  45. package/src/connectors/local-embedder.js +20 -0
  46. package/src/connectors/memory-retriever.js +18 -0
  47. package/src/connectors/ollama-connector.js +29 -2
  48. package/src/connectors/openai-connector.js +49 -4
  49. package/src/core/create-pipeline.js +112 -15
  50. package/src/core/performance/parallel-processor.js +62 -1
  51. package/src/core/plugin-registry.js +42 -19
  52. package/src/core/stage-contracts.js +36 -0
  53. package/src/core/validate-plugin.js +85 -0
  54. package/src/debug/execution-tracer.js +14 -4
  55. package/src/dev/dev-server.js +8 -1
  56. package/src/dev/hot-reload.js +4 -3
  57. package/src/enterprise/sso-integration.js +4 -3
  58. package/src/evaluate/semantic-similarity.js +79 -0
  59. package/src/grounding/check-answer.js +173 -0
  60. package/src/index.js +22 -0
  61. package/src/loader/csv-loader.js +3 -2
  62. package/src/loader/html-loader.js +3 -1
  63. package/src/loader/markdown-loader.js +3 -2
  64. package/src/retrieval/file-vector-store.js +30 -1
  65. package/src/security/jwt-validator.js +5 -4
  66. package/src/testing.js +254 -0
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * What each pipeline stage must provide, as createRagPipeline and the CLI
5
+ * use it. A plugin needs at least one of `anyOf`; a missing `recommended`
6
+ * method only matters for the command named in its note.
7
+ *
8
+ * Shared by validatePlugin() and the `rag-pipeline init` wizard. The
9
+ * stricter rules pluginRegistry.register() applies (metadata, and the
10
+ * methods in contracts/*.json) are checked by validatePlugin(..., {
11
+ * registry: true }).
12
+ */
13
+ const STAGE_CONTRACTS = Object.freeze({
14
+ loader: {
15
+ anyOf: ['load'],
16
+ recommended: {},
17
+ },
18
+ embedder: {
19
+ anyOf: ['embed', 'embedQuery'],
20
+ recommended: { embed: '`ingest` calls embed() to embed documents' },
21
+ },
22
+ retriever: {
23
+ anyOf: ['retrieve'],
24
+ recommended: { store: '`ingest` calls store() to save vectors' },
25
+ },
26
+ reranker: {
27
+ anyOf: ['rerank'],
28
+ recommended: {},
29
+ },
30
+ llm: {
31
+ anyOf: ['generate'],
32
+ recommended: {},
33
+ },
34
+ });
35
+
36
+ module.exports = { STAGE_CONTRACTS };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { STAGE_CONTRACTS } = require('./stage-contracts');
4
+
5
+ /**
6
+ * @typedef {Object} PluginValidationResult
7
+ * @property {boolean} valid - True when there are no errors
8
+ * @property {string[]} errors - Problems that stop the plugin from working
9
+ * @property {string[]} warnings - Methods some commands need, e.g. `store`
10
+ * for `rag-pipeline ingest`
11
+ */
12
+
13
+ /**
14
+ * Checks that an object can be used as a pipeline stage, without throwing,
15
+ * so plugin authors can assert on it in their own tests.
16
+ *
17
+ * By default it checks what createRagPipeline() calls. With
18
+ * `{ registry: true }` it also applies the rules pluginRegistry.register()
19
+ * enforces: `metadata` with a string `name` and `version` and a `type`
20
+ * equal to the stage, plus the methods in the stage's contract.
21
+ *
22
+ * @example
23
+ * const { validatePlugin } = require('@devilsdev/rag-pipeline-utils');
24
+ * const result = validatePlugin('llm', myLlm, { registry: true });
25
+ * expect(result.errors).toEqual([]);
26
+ *
27
+ * @param {'loader'|'embedder'|'retriever'|'reranker'|'llm'} stage
28
+ * @param {object} plugin - Plugin instance or object
29
+ * @param {Object} [options]
30
+ * @param {boolean} [options.registry=false] - Also check registry rules
31
+ * @returns {PluginValidationResult}
32
+ */
33
+ function validatePlugin(stage, plugin, options = {}) {
34
+ const contract = STAGE_CONTRACTS[stage];
35
+ if (!contract) {
36
+ return {
37
+ valid: false,
38
+ errors: [
39
+ `Unknown stage "${stage}". Expected one of: ${Object.keys(STAGE_CONTRACTS).join(', ')}`,
40
+ ],
41
+ warnings: [],
42
+ };
43
+ }
44
+
45
+ if (
46
+ plugin === null ||
47
+ (typeof plugin !== 'object' && typeof plugin !== 'function')
48
+ ) {
49
+ return {
50
+ valid: false,
51
+ errors: [`A ${stage} plugin must be an object or class instance`],
52
+ warnings: [],
53
+ };
54
+ }
55
+
56
+ const has = (method) => typeof plugin[method] === 'function';
57
+ const errors = [];
58
+ const warnings = [];
59
+
60
+ if (!contract.anyOf.some(has)) {
61
+ errors.push(
62
+ `A ${stage} needs ${contract.anyOf.map((m) => `${m}()`).join(' or ')}`,
63
+ );
64
+ }
65
+ for (const [method, why] of Object.entries(contract.recommended)) {
66
+ if (!has(method)) warnings.push(`No ${method}() method: ${why}`);
67
+ }
68
+
69
+ if (options.registry) {
70
+ // Reuse the registry's own check so the two cannot drift. It stops at
71
+ // the first problem, so this reports one registry error at a time.
72
+ // eslint-disable-next-line global-require
73
+ const { PluginRegistry } = require('./plugin-registry');
74
+ try {
75
+ new PluginRegistry()._validatePluginContract(stage, plugin);
76
+ } catch (error) {
77
+ const message = `pluginRegistry.register(): ${error.message}`;
78
+ if (!errors.includes(message)) errors.push(message);
79
+ }
80
+ }
81
+
82
+ return { valid: errors.length === 0, errors, warnings };
83
+ }
84
+
85
+ module.exports = { validatePlugin };
@@ -28,10 +28,17 @@ function generateTraceId() {
28
28
  }
29
29
 
30
30
  /**
31
- * Truncate a value if its JSON representation exceeds maxSize bytes
32
- * @param {*} value - Value to truncate
31
+ * Snapshot a value for a trace: a JSON copy, or a truncated string when its
32
+ * JSON exceeds maxSize characters.
33
+ *
34
+ * Always a copy, never the live object: the traced run attaches the trace to
35
+ * its result (`result.trace`), so storing the result itself made the trace
36
+ * circular and exportTraces('json') threw. A copy also keeps later changes to
37
+ * the result out of the recorded trace.
38
+ *
39
+ * @param {*} value - Value to snapshot
33
40
  * @param {number} maxSize - Maximum serialized size in characters
34
- * @returns {*} Original value or truncated string
41
+ * @returns {*} JSON copy of the value, or a truncated string
35
42
  */
36
43
  function truncatePayload(value, maxSize) {
37
44
  if (value === undefined || value === null) {
@@ -43,8 +50,11 @@ function truncatePayload(value, maxSize) {
43
50
  } catch {
44
51
  return '[unserializable]';
45
52
  }
53
+ if (serialized === undefined) {
54
+ return undefined; // functions and symbols have no JSON form
55
+ }
46
56
  if (serialized.length <= maxSize) {
47
- return value;
57
+ return JSON.parse(serialized);
48
58
  }
49
59
  return serialized.slice(0, maxSize) + '...[truncated]';
50
60
  }
@@ -356,7 +356,14 @@ ${this.options.watchPaths.map((p) => `║ - ${p.padEnd(56)}║`).join('\n')}
356
356
  const { config, configDir } = loadCliConfig({
357
357
  config: this.options.configPath,
358
358
  });
359
- return query({ config, configDir, question, topK });
359
+ // The HTTP response keeps its shape: answer, results, citations.
360
+ const { timings, durationMs, ...result } = await query({
361
+ config,
362
+ configDir,
363
+ question,
364
+ topK,
365
+ });
366
+ return result;
360
367
  }
361
368
 
362
369
  /** @private */
@@ -18,7 +18,8 @@
18
18
  */
19
19
 
20
20
  const { EventEmitter } = require('events');
21
- const chokidar = require('chokidar');
21
+ // chokidar loads when watching starts, not on import.
22
+ const loadChokidar = () => require('chokidar'); // eslint-disable-line global-require
22
23
  const path = require('path');
23
24
  const fs = require('fs');
24
25
  const {
@@ -170,7 +171,7 @@ class HotReloadManager extends EventEmitter {
170
171
  * @returns {Promise<void>}
171
172
  */
172
173
  async _watchPlugins() {
173
- const watcher = chokidar.watch(this.options.watchPaths, {
174
+ const watcher = loadChokidar().watch(this.options.watchPaths, {
174
175
  ignored: toIgnoreMatcher(this.options.ignorePatterns),
175
176
  persistent: true,
176
177
  ignoreInitial: true,
@@ -214,7 +215,7 @@ class HotReloadManager extends EventEmitter {
214
215
  return;
215
216
  }
216
217
 
217
- const watcher = chokidar.watch(this.options.configPath, {
218
+ const watcher = loadChokidar().watch(this.options.configPath, {
218
219
  persistent: true,
219
220
  ignoreInitial: true,
220
221
  awaitWriteFinish: {
@@ -17,7 +17,8 @@
17
17
 
18
18
  const crypto = require('crypto');
19
19
  const { EventEmitter } = require('events');
20
- const jwt = require('jsonwebtoken');
20
+ // jsonwebtoken loads on first use (about 90 ms otherwise, on every import).
21
+ const loadJwt = () => require('jsonwebtoken'); // eslint-disable-line global-require
21
22
  const RateLimiter = require('../utils/rate-limiter');
22
23
  const { logger } = require('../utils/logger');
23
24
  const { JWTValidator } = require('../security/jwt-validator');
@@ -1386,7 +1387,7 @@ class OIDCProvider {
1386
1387
  if (!this._config.clientId) {
1387
1388
  throw new Error('clientId must be configured');
1388
1389
  }
1389
- const decoded = jwt.decode(idToken, { complete: true });
1390
+ const decoded = loadJwt().decode(idToken, { complete: true });
1390
1391
  if (!decoded || !decoded.header || !decoded.payload) {
1391
1392
  throw new Error('malformed id_token');
1392
1393
  }
@@ -1410,7 +1411,7 @@ class OIDCProvider {
1410
1411
  throw new Error('id_token has no sub claim');
1411
1412
  }
1412
1413
 
1413
- const claims = jwt.verify(idToken, key, {
1414
+ const claims = loadJwt().verify(idToken, key, {
1414
1415
  algorithms: [header.alg],
1415
1416
  issuer: this._config.issuer,
1416
1417
  audience: this._config.clientId,
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Semantic similarity between an answer and a reference answer, from the
5
+ * cosine similarity of their embeddings. Unlike BLEU and ROUGE it does not
6
+ * penalise a correct answer that is worded differently.
7
+ *
8
+ * Embeddings capture topic and meaning, not facts: "twenty days" and
9
+ * "thirty days" score as very similar. Use it next to the grounding checks
10
+ * (computeFaithfulness, checkAnswer), not instead of them.
11
+ */
12
+
13
+ function cosine(a, b) {
14
+ if (
15
+ !Array.isArray(a) ||
16
+ !Array.isArray(b) ||
17
+ a.length !== b.length ||
18
+ a.length === 0
19
+ ) {
20
+ throw new Error(
21
+ `Embeddings must be non-empty arrays of the same length (got ${a && a.length} and ${b && b.length})`,
22
+ );
23
+ }
24
+ let dot = 0;
25
+ let na = 0;
26
+ let nb = 0;
27
+ for (let i = 0; i < a.length; i += 1) {
28
+ dot += a[i] * b[i];
29
+ na += a[i] * a[i];
30
+ nb += b[i] * b[i];
31
+ }
32
+ if (na === 0 || nb === 0) return 0;
33
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
34
+ }
35
+
36
+ async function embedText(embedder, text, signal) {
37
+ const options = signal ? { signal } : {};
38
+ if (typeof embedder.embedQuery === 'function') {
39
+ return embedder.embedQuery(text, options);
40
+ }
41
+ return embedder.embed(text, options);
42
+ }
43
+
44
+ /**
45
+ * @param {string} answer - The generated answer
46
+ * @param {string} reference - The reference (ground-truth) answer
47
+ * @param {Object} options
48
+ * @param {Object} options.embedder - Any embedder with embed() or embedQuery()
49
+ * (a built-in connector, LocalEmbedder fitted to your corpus, an AI SDK
50
+ * adapter, HashEmbedder from /testing)
51
+ * @param {AbortSignal} [options.signal]
52
+ * @returns {Promise<number>} Cosine similarity clamped to 0..1
53
+ */
54
+ async function computeSemanticSimilarity(
55
+ answer,
56
+ reference,
57
+ { embedder, signal } = {},
58
+ ) {
59
+ if (typeof answer !== 'string' || typeof reference !== 'string') {
60
+ throw new TypeError('answer and reference must be strings');
61
+ }
62
+ if (
63
+ !embedder ||
64
+ (typeof embedder.embed !== 'function' &&
65
+ typeof embedder.embedQuery !== 'function')
66
+ ) {
67
+ throw new TypeError(
68
+ 'computeSemanticSimilarity needs an embedder with embed() or embedQuery()',
69
+ );
70
+ }
71
+ if (answer.trim() === '' || reference.trim() === '') return 0;
72
+ const [a, b] = await Promise.all([
73
+ embedText(embedder, answer, signal),
74
+ embedText(embedder, reference, signal),
75
+ ]);
76
+ return Math.min(1, Math.max(0, cosine(a, b)));
77
+ }
78
+
79
+ module.exports = { computeSemanticSimilarity };
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Check an answer that any framework produced (LangChain, LlamaIndex, Mastra,
5
+ * the AI SDK, your own code) against the sources it was given: citations
6
+ * per sentence, groundedness, unsupported sentences and evaluation scores,
7
+ * without using createRagPipeline().
8
+ */
9
+
10
+ const { CitationTracker } = require('../citation/citation-tracker');
11
+ const { PipelineEvaluator } = require('../evaluate/pipeline-evaluator');
12
+ const {
13
+ computeSemanticSimilarity,
14
+ } = require('../evaluate/semantic-similarity');
15
+
16
+ /**
17
+ * Text of one source in the shapes common frameworks use.
18
+ * @private
19
+ */
20
+ function sourceText(source) {
21
+ if (source == null) return '';
22
+ if (typeof source === 'string') return source;
23
+ if (typeof source.content === 'string') return source.content; // this library
24
+ if (typeof source.pageContent === 'string') return source.pageContent; // LangChain Document
25
+ if (typeof source.text === 'string') return source.text; // LlamaIndex node, plain
26
+ if (source.node) return sourceText(source.node); // LlamaIndex NodeWithScore
27
+ if (typeof source.getContent === 'function')
28
+ return String(source.getContent());
29
+ return '';
30
+ }
31
+
32
+ /**
33
+ * An id for a source: its own id when it has one, otherwise its position.
34
+ * @private
35
+ */
36
+ function sourceId(source, index) {
37
+ const candidates = [
38
+ source && source.id,
39
+ source && source.id_,
40
+ source && source.node && (source.node.id_ || source.node.id),
41
+ source && source.metadata && (source.metadata.id || source.metadata.source),
42
+ ];
43
+ const id = candidates.find(
44
+ (c) => typeof c === 'string' || typeof c === 'number',
45
+ );
46
+ return id !== undefined ? String(id) : String(index + 1);
47
+ }
48
+
49
+ /**
50
+ * Normalize sources to `{ id, content, metadata }`.
51
+ *
52
+ * Accepts strings, `{ id, content }` (this library), LangChain Documents
53
+ * (`{ pageContent, metadata }`), LlamaIndex nodes (`{ text }`) and
54
+ * `NodeWithScore` (`{ node, score }`).
55
+ *
56
+ * @param {Array<string|Object>} sources
57
+ * @returns {Array<{id: string, content: string, metadata: Object}>}
58
+ */
59
+ function normalizeSources(sources) {
60
+ if (!Array.isArray(sources)) {
61
+ throw new TypeError('sources must be an array');
62
+ }
63
+ return sources
64
+ .map((source, index) => ({
65
+ id: sourceId(source, index),
66
+ content: sourceText(source),
67
+ metadata:
68
+ (source &&
69
+ (source.metadata || (source.node && source.node.metadata))) ||
70
+ {},
71
+ }))
72
+ .filter((s) => s.content.trim() !== '');
73
+ }
74
+
75
+ /**
76
+ * @typedef {Object} AnswerCheck
77
+ * @property {boolean} grounded - Every sentence is supported by a source
78
+ * @property {number} groundedness - Share of supported sentences, 0..1
79
+ * @property {string[]} unsupported - Sentences no source supports (this
80
+ * includes a sentence whose numbers do not match its source)
81
+ * @property {Array<{sentence: string, sources: Array}>} citations - Sources
82
+ * supporting each sentence
83
+ * @property {{scores: Object, details: Object, metadata: Object}|undefined} evaluation
84
+ * - Faithfulness, relevance, context precision (and context recall when a
85
+ * referenceAnswer is given); only when `evaluate` is not false
86
+ * @property {Array<{id: string, content: string, metadata: Object}>} sources
87
+ * - The sources as they were checked
88
+ */
89
+
90
+ /**
91
+ * Check an answer against the sources it was generated from.
92
+ *
93
+ * @example
94
+ * // A LangChain chain's output
95
+ * const check = await checkAnswer({
96
+ * question: input,
97
+ * answer: result.answer,
98
+ * sources: result.context, // LangChain Documents
99
+ * });
100
+ * if (!check.grounded) console.warn(check.unsupported);
101
+ *
102
+ * @param {Object} params
103
+ * @param {string} params.answer - The generated answer
104
+ * @param {Array<string|Object>} params.sources - Documents the answer was
105
+ * generated from (see normalizeSources for accepted shapes)
106
+ * @param {string} [params.question] - The question; needed for relevance
107
+ * @param {string} [params.referenceAnswer] - Ground truth, for context recall
108
+ * (and semantic similarity when an embedder is given)
109
+ * @param {Object} [params.embedder] - With a referenceAnswer, adds
110
+ * `evaluation.scores.semanticSimilarity` (computeSemanticSimilarity)
111
+ * @param {boolean} [params.evaluate=true] - Also compute evaluation scores
112
+ * @returns {Promise<AnswerCheck>}
113
+ */
114
+ async function checkAnswer({
115
+ answer,
116
+ sources,
117
+ question,
118
+ referenceAnswer,
119
+ embedder,
120
+ evaluate = true,
121
+ } = {}) {
122
+ if (typeof answer !== 'string') {
123
+ throw new TypeError('answer must be a string');
124
+ }
125
+ const docs = normalizeSources(sources);
126
+
127
+ // Models sometimes return nothing; that is an ungrounded answer, not an
128
+ // error. (CitationTracker rejects empty text.)
129
+ if (answer.trim() === '') {
130
+ return {
131
+ grounded: false,
132
+ groundedness: 0,
133
+ unsupported: [],
134
+ citations: [],
135
+ evaluation: undefined,
136
+ sources: docs,
137
+ };
138
+ }
139
+
140
+ const tracked = new CitationTracker().track(answer, docs);
141
+ const sentences =
142
+ (tracked.hallucinationReport && tracked.hallucinationReport.sentences) ||
143
+ [];
144
+ const unsupported = sentences
145
+ .filter((s) => s.classification !== 'grounded')
146
+ .map((s) => s.text);
147
+
148
+ let evaluation;
149
+ if (evaluate) {
150
+ evaluation = await new PipelineEvaluator().evaluate(
151
+ { query: question || '', answer, results: docs },
152
+ referenceAnswer ? { referenceAnswer } : {},
153
+ );
154
+ if (referenceAnswer && embedder) {
155
+ evaluation.scores.semanticSimilarity = await computeSemanticSimilarity(
156
+ answer,
157
+ referenceAnswer,
158
+ { embedder },
159
+ );
160
+ }
161
+ }
162
+
163
+ return {
164
+ grounded: sentences.length > 0 && unsupported.length === 0,
165
+ groundedness: tracked.groundednessScore,
166
+ unsupported,
167
+ citations: tracked.citations,
168
+ evaluation,
169
+ sources: docs,
170
+ };
171
+ }
172
+
173
+ module.exports = { checkAnswer, normalizeSources };
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ const { normalizeConfig } = require('./config/normalize-config');
13
13
 
14
14
  // Plugin system
15
15
  const pluginRegistry = require('./core/plugin-registry');
16
+ const { validatePlugin } = require('./core/validate-plugin');
16
17
 
17
18
  // Utilities
18
19
  const logger = require('./utils/logger');
@@ -59,6 +60,7 @@ const {
59
60
  computeContextRecall,
60
61
  } = require('./evaluate/context-metrics');
61
62
  const { computeGroundedness } = require('./evaluate/groundedness');
63
+ const { computeSemanticSimilarity } = require('./evaluate/semantic-similarity');
62
64
  const { PipelineEvaluator } = require('./evaluate/pipeline-evaluator');
63
65
 
64
66
  // Chunking
@@ -66,6 +68,10 @@ const { ChunkingEngine } = require('./chunking/chunking-engine');
66
68
 
67
69
  // Citation & Grounding
68
70
  const { CitationTracker } = require('./citation/citation-tracker');
71
+ const { checkAnswer, normalizeSources } = require('./grounding/check-answer');
72
+ const { createAiSdkAdapter } = require('./adapters/ai-sdk');
73
+ const { MemoryCache } = require('./cache/memory-cache');
74
+ const { RedisCache } = require('./cache/redis-cache');
69
75
  const {
70
76
  mapSentenceToSources,
71
77
  buildIDFWeights,
@@ -188,6 +194,7 @@ module.exports = {
188
194
 
189
195
  // Plugin system
190
196
  pluginRegistry,
197
+ validatePlugin,
191
198
 
192
199
  // Utilities
193
200
  logger,
@@ -225,6 +232,7 @@ module.exports = {
225
232
  checkNumericConsistency,
226
233
  extractNumbers,
227
234
  computeFaithfulness,
235
+ computeSemanticSimilarity,
228
236
  computeAnswerRelevance,
229
237
  computeContextPrecision,
230
238
  computeContextRecall,
@@ -236,6 +244,11 @@ module.exports = {
236
244
 
237
245
  // Citation & Grounding
238
246
  CitationTracker,
247
+ checkAnswer,
248
+ normalizeSources,
249
+ createAiSdkAdapter,
250
+ MemoryCache,
251
+ RedisCache,
239
252
  mapSentenceToSources,
240
253
  buildIDFWeights,
241
254
  detectHallucinations,
@@ -358,6 +371,15 @@ for (const name of Object.keys(experimental)) {
358
371
  });
359
372
  }
360
373
 
374
+ // The ./testing build entry reads its module from here, so it shares the
375
+ // main bundle's classes (MemoryRetriever, createRagPipeline). A getter, so
376
+ // importing the package does not load the testing helpers.
377
+ Object.defineProperty(
378
+ module.exports,
379
+ Symbol.for('@devilsdev/rag-pipeline-utils/testing'),
380
+ { get: () => require('./testing'), enumerable: false }, // eslint-disable-line global-require
381
+ );
382
+
361
383
  // The ./experimental build entry reads the unwrapped classes from here, so
362
384
  // both entries share one copy of each class (non-enumerable: not a named export).
363
385
  Object.defineProperty(
@@ -4,7 +4,8 @@
4
4
 
5
5
  const fs = require('fs/promises');
6
6
  const path = require('path');
7
- const { parse } = require('csv-parse/sync');
7
+ // csv-parse loads when a CSV file is read, not on import.
8
+ const loadCsvParse = () => require('csv-parse/sync'); // eslint-disable-line global-require
8
9
 
9
10
  class CSVLoader {
10
11
  /**
@@ -20,7 +21,7 @@ class CSVLoader {
20
21
  const src = raw.replace(/^\uFEFF/, ''); // Remove BOM
21
22
  const normalizedSrc = src.replace(/\r\n|\r/g, '\n'); // Normalize line endings
22
23
 
23
- const records = parse(normalizedSrc, { columns: true });
24
+ const records = loadCsvParse().parse(normalizedSrc, { columns: true });
24
25
 
25
26
  return [
26
27
  {
@@ -4,7 +4,8 @@
4
4
 
5
5
  const fs = require('fs/promises');
6
6
  const path = require('path');
7
- const { JSDOM } = require('jsdom');
7
+ // jsdom (about 100 packages) loads when an HTML file is read, not on import.
8
+ const loadJsdom = () => require('jsdom'); // eslint-disable-line global-require
8
9
 
9
10
  /**
10
11
  * HTMLLoader reads HTML files and extracts visible text.
@@ -19,6 +20,7 @@ class HTMLLoader {
19
20
  async load(filePath) {
20
21
  const absPath = path.resolve(filePath);
21
22
  const raw = await fs.readFile(absPath, 'utf-8');
23
+ const { JSDOM } = loadJsdom();
22
24
  const dom = new JSDOM(raw);
23
25
  let text = dom.window.document.body.textContent || '';
24
26
 
@@ -10,7 +10,8 @@ const fs = require('fs/promises');
10
10
  // eslint-disable-line global-require
11
11
  const path = require('path');
12
12
  // eslint-disable-line global-require
13
- const { marked } = require('marked');
13
+ // marked loads when a Markdown file is read, not on import.
14
+ const loadMarked = () => require('marked'); // eslint-disable-line global-require
14
15
  // eslint-disable-line global-require
15
16
 
16
17
  /**
@@ -26,7 +27,7 @@ class MarkdownLoader {
26
27
  async load(_filePath) {
27
28
  const absPath = path.resolve(_filePath);
28
29
  const raw = await fs.readFile(absPath, 'utf-8');
29
- const html = marked(raw);
30
+ const html = loadMarked().marked(raw);
30
31
  const text = html.replace(/<[^>]+>/g, ''); // strip tags
31
32
 
32
33
  return [