@devilsdev/rag-pipeline-utils 2.6.1 → 2.7.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 (90) hide show
  1. package/BENCHMARKS.md +7 -1
  2. package/CHANGELOG.md +135 -0
  3. package/README.md +37 -24
  4. package/dist/index.cjs +4164 -1825
  5. package/dist/index.d.ts +265 -256
  6. package/dist/index.mjs +1 -1
  7. package/dist/types/cache/memory-cache.d.ts +4 -1
  8. package/dist/types/cache/redis-cache.d.ts +4 -1
  9. package/dist/types/cli/services/instrument.d.ts +14 -0
  10. package/dist/types/cli/services/rag-service.d.ts +140 -0
  11. package/dist/types/cli/shared/config-context.d.ts +9 -0
  12. package/dist/types/config/component-factory.d.ts +57 -0
  13. package/dist/types/config/enhanced-ragrc-schema.d.ts +648 -0
  14. package/dist/types/config/load-config.d.ts +18 -0
  15. package/dist/types/config/normalize-config.d.ts +9 -0
  16. package/dist/types/core/observability/event-logger.d.ts +211 -0
  17. package/dist/types/core/observability/metrics.d.ts +585 -0
  18. package/dist/types/core/performance/parallel-processor.d.ts +92 -0
  19. package/dist/types/core/plugin-registry.d.ts +5 -0
  20. package/dist/types/dag/core/execution-scheduler.d.ts +3 -1
  21. package/dist/types/dag/dag-engine.d.ts +15 -2
  22. package/dist/types/dev/dev-server.d.ts +136 -0
  23. package/dist/types/dev/hot-reload.d.ts +226 -0
  24. package/dist/types/ecosystem/plugin-analytics-dashboard.d.ts +141 -0
  25. package/dist/types/ecosystem/plugin-hub.d.ts +161 -0
  26. package/dist/types/ecosystem/plugin-installer.d.ts +40 -0
  27. package/dist/types/ecosystem/plugin-probe.d.ts +21 -0
  28. package/dist/types/ecosystem/plugin-scanner.d.ts +30 -0
  29. package/dist/types/ecosystem/tar-extract.d.ts +24 -0
  30. package/dist/types/enterprise/audit-logging.d.ts +217 -0
  31. package/dist/types/evaluate/compare.d.ts +140 -0
  32. package/dist/types/evaluate/dataset.d.ts +43 -0
  33. package/dist/types/evaluate/evaluator.d.ts +14 -0
  34. package/dist/types/evaluate/report.d.ts +56 -0
  35. package/dist/types/evaluate/synthetic.d.ts +64 -0
  36. package/dist/types/loader/csv-loader.d.ts +10 -0
  37. package/dist/types/loader/document-reader.d.ts +18 -0
  38. package/dist/types/loader/html-loader.d.ts +21 -0
  39. package/dist/types/loader/markdown-loader.d.ts +21 -0
  40. package/dist/types/retrieval/bm25-search.d.ts +4 -0
  41. package/dist/types/retrieval/file-vector-store.d.ts +4 -1
  42. package/dist/types/retrieval/hybrid-retriever.d.ts +4 -0
  43. package/dist/types/retrieval/pgvector-retriever.d.ts +123 -0
  44. package/dist/types/retrieval/qdrant-retriever.d.ts +106 -0
  45. package/dist/types/security/validator.d.ts +11 -1
  46. package/dist/types/testing.d.ts +2 -0
  47. package/dist/types/utils/batch-processor.d.ts +205 -0
  48. package/dist/types/utils/connection-pool.d.ts +158 -0
  49. package/dist/types/utils/error-formatter.d.ts +312 -0
  50. package/dist/types/utils/logger.d.ts +29 -4
  51. package/dist/types/utils/rate-limiter.d.ts +13 -8
  52. package/dist/types/utils/retry.d.ts +10 -0
  53. package/package.json +13 -3
  54. package/src/cache/memory-cache.js +4 -1
  55. package/src/cache/redis-cache.js +4 -1
  56. package/src/cli/enhanced-cli-commands.js +79 -0
  57. package/src/cli/handlers/ai-handler.js +2 -2
  58. package/src/cli/handlers/eval-handler.js +201 -0
  59. package/src/cli/handlers/generate-handler.js +103 -0
  60. package/src/cli/services/benchmark.js +2 -2
  61. package/src/cli/services/rag-service.js +59 -10
  62. package/src/cli/shared/cli-utils.js +40 -1
  63. package/src/config/component-factory.js +32 -0
  64. package/src/connectors/local-embedder.js +20 -13
  65. package/src/core/observability/event-logger.js +135 -1
  66. package/src/core/observability/metrics.js +223 -5
  67. package/src/core/performance/parallel-processor.js +2 -0
  68. package/src/core/plugin-registry.js +5 -1
  69. package/src/dag/core/execution-scheduler.js +2 -0
  70. package/src/dag/dag-engine.js +13 -0
  71. package/src/dev/dev-server.js +6 -0
  72. package/src/ecosystem/plugin-hub.js +4 -0
  73. package/src/enterprise/audit-logging.js +5 -0
  74. package/src/evaluate/compare.js +395 -0
  75. package/src/evaluate/dataset.js +207 -0
  76. package/src/evaluate/report.js +350 -0
  77. package/src/evaluate/synthetic.js +246 -0
  78. package/src/index.js +17 -0
  79. package/src/retrieval/bm25-search.js +4 -0
  80. package/src/retrieval/file-vector-store.js +4 -1
  81. package/src/retrieval/hybrid-retriever.js +4 -0
  82. package/src/retrieval/pgvector-retriever.js +420 -0
  83. package/src/retrieval/qdrant-retriever.js +361 -0
  84. package/src/security/validator.js +8 -0
  85. package/src/testing.js +2 -0
  86. package/src/utils/batch-processor.js +7 -3
  87. package/src/utils/error-formatter.js +48 -20
  88. package/src/utils/logger.js +31 -1
  89. package/src/utils/rate-limiter.js +9 -4
  90. package/src/utils/retry.js +10 -1
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Generates an evaluation dataset from a corpus: an LLM writes question and
5
+ * answer pairs about one document at a time, and each case records that
6
+ * document as its source. The output is a dataset validateDataset,
7
+ * runEvaluation and `rag-pipeline eval` read.
8
+ *
9
+ * Generated cases are a starting point, not ground truth. Each one is
10
+ * marked `reviewed: false` and tagged "synthetic"; read them, fix or delete
11
+ * the bad ones, and set `reviewed: true`.
12
+ */
13
+
14
+ const { computeFaithfulness } = require('./faithfulness');
15
+
16
+ const DEFAULT_PROMPT = [
17
+ 'Write {count} question(s) that the reference document answers, each with',
18
+ 'its answer. Each question must make sense on its own, as a user of a',
19
+ 'search system would ask it, without mentioning "the document" or "the',
20
+ 'text". Each answer must be one or two sentences using only facts stated',
21
+ 'in the document. Reply with a JSON array and nothing else, in this form:',
22
+ '[{"question": "...", "answer": "..."}]',
23
+ ].join(' ');
24
+
25
+ const MIN_DOCUMENT_LENGTH = 80;
26
+
27
+ const textOf = (doc) =>
28
+ typeof doc === 'string'
29
+ ? doc
30
+ : String((doc && (doc.content || doc.text)) || '');
31
+
32
+ /** The id a case's sourceIds should hold: what retrieval results carry. */
33
+ function sourceIdOf(doc, index) {
34
+ if (doc && typeof doc === 'object') {
35
+ if (doc.metadata && doc.metadata.source !== undefined) {
36
+ return String(doc.metadata.source);
37
+ }
38
+ if (doc.id !== undefined && doc.id !== null) return String(doc.id);
39
+ }
40
+ return `doc-${index + 1}`;
41
+ }
42
+
43
+ /**
44
+ * Pulls the JSON array of pairs out of a model reply. Accepts a bare array,
45
+ * one inside a ```json fence, or one surrounded by prose; returns null when
46
+ * there is none.
47
+ */
48
+ function parsePairs(reply) {
49
+ if (typeof reply !== 'string') return null;
50
+ const candidates = [];
51
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(reply);
52
+ if (fenced) candidates.push(fenced[1]);
53
+ candidates.push(reply);
54
+ const start = reply.indexOf('[');
55
+ const end = reply.lastIndexOf(']');
56
+ if (start >= 0 && end > start) candidates.push(reply.slice(start, end + 1));
57
+ for (const text of candidates) {
58
+ try {
59
+ const parsed = JSON.parse(text.trim());
60
+ if (Array.isArray(parsed)) return parsed;
61
+ if (parsed && Array.isArray(parsed.questions)) return parsed.questions;
62
+ } catch {
63
+ // try the next candidate
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ const normalise = (question) =>
70
+ question
71
+ .toLowerCase()
72
+ .replace(/[^\p{L}\p{N} ]/gu, ' ')
73
+ .replace(/\s+/g, ' ')
74
+ .trim();
75
+
76
+ /**
77
+ * Generates question and answer pairs from documents with an LLM.
78
+ *
79
+ * Each document gets its own request, with that document as the only
80
+ * context, so every case knows its source. Generated answers are checked
81
+ * against the document with computeFaithfulness, and pairs whose answer the
82
+ * document doesn't support, malformed pairs and repeated questions are
83
+ * dropped and listed in `skipped`.
84
+ *
85
+ * @param {Array<{id?: string, content: string, metadata?: Object}|string>} documents - The corpus, e.g. the chunks you index
86
+ * @param {Object} options
87
+ * @param {{ generate: Function }} options.llm - Any llm stage: `generate(query, docs, options)` returning a string
88
+ * @param {number} [options.count=20] - Cases to generate at most
89
+ * @param {number} [options.perDocument=1] - Questions to ask per document
90
+ * @param {number} [options.minFaithfulness=1] - Share of an answer's sentences the document must support
91
+ * @param {number} [options.minLength=80] - Documents shorter than this many characters are skipped
92
+ * @param {string} [options.prompt] - Replaces the instruction; `{count}` is replaced by perDocument
93
+ * @param {number} [options.concurrency=1] - Documents processed at the same time
94
+ * @param {Object} [options.generateOptions] - Extra options for every `llm.generate()` call, e.g. `{ temperature: 0.7 }`
95
+ * @param {AbortSignal} [options.signal]
96
+ * @param {(testCase: Object) => void} [options.onCase] - Called for each case kept
97
+ * @returns {Promise<{
98
+ * cases: Array<{ id: string, prompt: string, expected: string, sourceIds: string[], tags: string[], reviewed: boolean }>,
99
+ * skipped: Array<{ sourceId: string, reason: string, question?: string }>
100
+ * }>} A dataset (`{ cases }`) plus what was dropped and why
101
+ */
102
+ async function generateSyntheticDataset(documents, options = {}) {
103
+ const {
104
+ llm,
105
+ count = 20,
106
+ perDocument = 1,
107
+ minFaithfulness = 1,
108
+ minLength = MIN_DOCUMENT_LENGTH,
109
+ prompt = DEFAULT_PROMPT,
110
+ concurrency = 1,
111
+ generateOptions = {},
112
+ signal,
113
+ onCase,
114
+ } = options;
115
+ if (!Array.isArray(documents)) {
116
+ throw new TypeError('documents must be an array');
117
+ }
118
+ if (!llm || typeof llm.generate !== 'function') {
119
+ throw new TypeError(
120
+ 'generateSyntheticDataset needs an llm with generate()',
121
+ );
122
+ }
123
+ for (const [name, value] of [
124
+ ['count', count],
125
+ ['perDocument', perDocument],
126
+ ['concurrency', concurrency],
127
+ ]) {
128
+ if (!Number.isInteger(value) || value < 1) {
129
+ throw new TypeError(`${name} must be a positive integer`);
130
+ }
131
+ }
132
+
133
+ const cases = [];
134
+ const skipped = [];
135
+ const seen = new Set();
136
+ const usedIds = new Set();
137
+
138
+ const eligible = [];
139
+ documents.forEach((doc, index) => {
140
+ const sourceId = sourceIdOf(doc, index);
141
+ if (textOf(doc).trim().length < minLength) {
142
+ skipped.push({
143
+ sourceId,
144
+ reason: `shorter than ${minLength} characters`,
145
+ });
146
+ } else {
147
+ eligible.push({ doc, sourceId });
148
+ }
149
+ });
150
+
151
+ const instruction = prompt.replace(/\{count\}/g, String(perDocument));
152
+
153
+ const addPair = (pair, { doc, sourceId }) => {
154
+ if (cases.length >= count) return;
155
+ const question =
156
+ pair && typeof pair.question === 'string' ? pair.question.trim() : '';
157
+ const answer =
158
+ pair && typeof pair.answer === 'string' ? pair.answer.trim() : '';
159
+ if (!question || !answer) {
160
+ skipped.push({ sourceId, reason: 'a pair without a question or answer' });
161
+ return;
162
+ }
163
+ const key = normalise(question);
164
+ if (seen.has(key)) {
165
+ skipped.push({
166
+ sourceId,
167
+ question,
168
+ reason: 'repeats an earlier question',
169
+ });
170
+ return;
171
+ }
172
+ const support = computeFaithfulness(answer, [{ content: textOf(doc) }]);
173
+ if (support.score < minFaithfulness) {
174
+ skipped.push({
175
+ sourceId,
176
+ question,
177
+ reason: `the document does not support the answer (faithfulness ${support.score.toFixed(2)})`,
178
+ });
179
+ return;
180
+ }
181
+ seen.add(key);
182
+ let id = `synthetic-${sourceId}`;
183
+ for (let n = 2; usedIds.has(id); n += 1) id = `synthetic-${sourceId}-${n}`;
184
+ usedIds.add(id);
185
+ const testCase = {
186
+ id,
187
+ prompt: question,
188
+ expected: answer,
189
+ sourceIds: [sourceId],
190
+ tags: ['synthetic'],
191
+ reviewed: false,
192
+ };
193
+ cases.push(testCase);
194
+ if (onCase) onCase(testCase);
195
+ };
196
+
197
+ const ask = async (entry) => {
198
+ let reply;
199
+ try {
200
+ reply = await llm.generate(instruction, [entry.doc], {
201
+ ...generateOptions,
202
+ ...(signal && { signal }),
203
+ });
204
+ } catch (err) {
205
+ if (signal && signal.aborted) throw err;
206
+ skipped.push({
207
+ sourceId: entry.sourceId,
208
+ reason: `the llm failed: ${err && err.message}`,
209
+ });
210
+ return;
211
+ }
212
+ const pairs = parsePairs(reply);
213
+ if (!pairs) {
214
+ skipped.push({
215
+ sourceId: entry.sourceId,
216
+ reason: 'the reply had no JSON array of pairs',
217
+ });
218
+ return;
219
+ }
220
+ for (const pair of pairs.slice(0, perDocument)) addPair(pair, entry);
221
+ };
222
+
223
+ let next = 0;
224
+ const worker = async () => {
225
+ while (next < eligible.length && cases.length < count) {
226
+ if (signal && signal.aborted) return;
227
+ const entry = eligible[next];
228
+ next += 1;
229
+ await ask(entry);
230
+ }
231
+ };
232
+ await Promise.all(
233
+ Array.from({ length: Math.min(concurrency, eligible.length) }, worker),
234
+ );
235
+ if (signal && signal.aborted) {
236
+ const err = new Error('Generation aborted');
237
+ err.code = 'ABORT_ERR';
238
+ throw err;
239
+ }
240
+ // Keep the corpus order, whatever order concurrent requests finished in.
241
+ const order = new Map(eligible.map((e, i) => [e.sourceId, i]));
242
+ cases.sort((a, b) => order.get(a.sourceIds[0]) - order.get(b.sourceIds[0]));
243
+ return { cases, skipped };
244
+ }
245
+
246
+ module.exports = { generateSyntheticDataset, parsePairs };
package/src/index.js CHANGED
@@ -61,6 +61,13 @@ const {
61
61
  } = require('./evaluate/context-metrics');
62
62
  const { computeGroundedness } = require('./evaluate/groundedness');
63
63
  const { computeSemanticSimilarity } = require('./evaluate/semantic-similarity');
64
+ const { validateDataset } = require('./evaluate/dataset');
65
+ const { generateSyntheticDataset } = require('./evaluate/synthetic');
66
+ const { runEvaluation, compareConfigurations } = require('./evaluate/compare');
67
+ const {
68
+ evaluationVerdict,
69
+ renderEvaluationReport,
70
+ } = require('./evaluate/report');
64
71
  const { PipelineEvaluator } = require('./evaluate/pipeline-evaluator');
65
72
 
66
73
  // Chunking
@@ -124,6 +131,8 @@ const { BaseConnector } = require('./connectors/base-connector');
124
131
  const { OllamaConnector } = require('./connectors/ollama-connector');
125
132
  const { LocalEmbedder } = require('./connectors/local-embedder');
126
133
  const { MemoryRetriever } = require('./connectors/memory-retriever');
134
+ const { PgVectorRetriever } = require('./retrieval/pgvector-retriever');
135
+ const { QdrantRetriever } = require('./retrieval/qdrant-retriever');
127
136
  const { OpenAIConnector } = require('./connectors/openai-connector');
128
137
  const { AnthropicConnector } = require('./connectors/anthropic-connector');
129
138
  const { CohereConnector } = require('./connectors/cohere-connector');
@@ -233,6 +242,12 @@ module.exports = {
233
242
  extractNumbers,
234
243
  computeFaithfulness,
235
244
  computeSemanticSimilarity,
245
+ validateDataset,
246
+ generateSyntheticDataset,
247
+ runEvaluation,
248
+ compareConfigurations,
249
+ evaluationVerdict,
250
+ renderEvaluationReport,
236
251
  computeAnswerRelevance,
237
252
  computeContextPrecision,
238
253
  computeContextRecall,
@@ -300,6 +315,8 @@ module.exports = {
300
315
  OllamaConnector,
301
316
  LocalEmbedder,
302
317
  MemoryRetriever,
318
+ PgVectorRetriever,
319
+ QdrantRetriever,
303
320
  OpenAIConnector,
304
321
  AnthropicConnector,
305
322
  CohereConnector,
@@ -6,6 +6,10 @@ const scoring = require('../evaluate/scoring');
6
6
 
7
7
  const DEFAULT_CONFIG = { k1: 1.2, b: 0.75 };
8
8
 
9
+ /**
10
+ * Keyword search with BM25 ranking. Index documents with index(), then
11
+ * search(query) returns them scored by term frequency and rarity.
12
+ */
9
13
  class BM25Search extends EventEmitter {
10
14
  constructor(options = {}) {
11
15
  super();
@@ -89,7 +89,10 @@ class FileVectorStore {
89
89
  return this.load().embedder;
90
90
  }
91
91
 
92
- /** @param {{name: string, dimensions: number, state?: object}} info */
92
+ /**
93
+ * Records which embedder wrote the index, saved with it.
94
+ * @param {{name: string, dimensions: number, state?: object}} info
95
+ */
93
96
  setEmbedderInfo(info) {
94
97
  this.load().embedder = info;
95
98
  this._revision += 1;
@@ -8,6 +8,10 @@ const DEFAULT_CONFIG = {
8
8
  fusionK: 60,
9
9
  };
10
10
 
11
+ /**
12
+ * Runs several retrievers in parallel (for example vector and BM25) and
13
+ * fuses their results with weighted reciprocal rank fusion.
14
+ */
11
15
  class HybridRetriever extends EventEmitter {
12
16
  /**
13
17
  * @param {Array<{retriever: object, weight: number, name: string}>} retrievers