@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.
- package/BENCHMARKS.md +7 -1
- package/CHANGELOG.md +135 -0
- package/README.md +37 -24
- package/dist/index.cjs +4164 -1825
- package/dist/index.d.ts +265 -256
- package/dist/index.mjs +1 -1
- package/dist/types/cache/memory-cache.d.ts +4 -1
- package/dist/types/cache/redis-cache.d.ts +4 -1
- package/dist/types/cli/services/instrument.d.ts +14 -0
- package/dist/types/cli/services/rag-service.d.ts +140 -0
- package/dist/types/cli/shared/config-context.d.ts +9 -0
- package/dist/types/config/component-factory.d.ts +57 -0
- package/dist/types/config/enhanced-ragrc-schema.d.ts +648 -0
- package/dist/types/config/load-config.d.ts +18 -0
- package/dist/types/config/normalize-config.d.ts +9 -0
- package/dist/types/core/observability/event-logger.d.ts +211 -0
- package/dist/types/core/observability/metrics.d.ts +585 -0
- package/dist/types/core/performance/parallel-processor.d.ts +92 -0
- package/dist/types/core/plugin-registry.d.ts +5 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +3 -1
- package/dist/types/dag/dag-engine.d.ts +15 -2
- package/dist/types/dev/dev-server.d.ts +136 -0
- package/dist/types/dev/hot-reload.d.ts +226 -0
- package/dist/types/ecosystem/plugin-analytics-dashboard.d.ts +141 -0
- package/dist/types/ecosystem/plugin-hub.d.ts +161 -0
- package/dist/types/ecosystem/plugin-installer.d.ts +40 -0
- package/dist/types/ecosystem/plugin-probe.d.ts +21 -0
- package/dist/types/ecosystem/plugin-scanner.d.ts +30 -0
- package/dist/types/ecosystem/tar-extract.d.ts +24 -0
- package/dist/types/enterprise/audit-logging.d.ts +217 -0
- package/dist/types/evaluate/compare.d.ts +140 -0
- package/dist/types/evaluate/dataset.d.ts +43 -0
- package/dist/types/evaluate/evaluator.d.ts +14 -0
- package/dist/types/evaluate/report.d.ts +56 -0
- package/dist/types/evaluate/synthetic.d.ts +64 -0
- package/dist/types/loader/csv-loader.d.ts +10 -0
- package/dist/types/loader/document-reader.d.ts +18 -0
- package/dist/types/loader/html-loader.d.ts +21 -0
- package/dist/types/loader/markdown-loader.d.ts +21 -0
- package/dist/types/retrieval/bm25-search.d.ts +4 -0
- package/dist/types/retrieval/file-vector-store.d.ts +4 -1
- package/dist/types/retrieval/hybrid-retriever.d.ts +4 -0
- package/dist/types/retrieval/pgvector-retriever.d.ts +123 -0
- package/dist/types/retrieval/qdrant-retriever.d.ts +106 -0
- package/dist/types/security/validator.d.ts +11 -1
- package/dist/types/testing.d.ts +2 -0
- package/dist/types/utils/batch-processor.d.ts +205 -0
- package/dist/types/utils/connection-pool.d.ts +158 -0
- package/dist/types/utils/error-formatter.d.ts +312 -0
- package/dist/types/utils/logger.d.ts +29 -4
- package/dist/types/utils/rate-limiter.d.ts +13 -8
- package/dist/types/utils/retry.d.ts +10 -0
- package/package.json +13 -3
- package/src/cache/memory-cache.js +4 -1
- package/src/cache/redis-cache.js +4 -1
- package/src/cli/enhanced-cli-commands.js +79 -0
- package/src/cli/handlers/ai-handler.js +2 -2
- package/src/cli/handlers/eval-handler.js +201 -0
- package/src/cli/handlers/generate-handler.js +103 -0
- package/src/cli/services/benchmark.js +2 -2
- package/src/cli/services/rag-service.js +59 -10
- package/src/cli/shared/cli-utils.js +40 -1
- package/src/config/component-factory.js +32 -0
- package/src/connectors/local-embedder.js +20 -13
- package/src/core/observability/event-logger.js +135 -1
- package/src/core/observability/metrics.js +223 -5
- package/src/core/performance/parallel-processor.js +2 -0
- package/src/core/plugin-registry.js +5 -1
- package/src/dag/core/execution-scheduler.js +2 -0
- package/src/dag/dag-engine.js +13 -0
- package/src/dev/dev-server.js +6 -0
- package/src/ecosystem/plugin-hub.js +4 -0
- package/src/enterprise/audit-logging.js +5 -0
- package/src/evaluate/compare.js +395 -0
- package/src/evaluate/dataset.js +207 -0
- package/src/evaluate/report.js +350 -0
- package/src/evaluate/synthetic.js +246 -0
- package/src/index.js +17 -0
- package/src/retrieval/bm25-search.js +4 -0
- package/src/retrieval/file-vector-store.js +4 -1
- package/src/retrieval/hybrid-retriever.js +4 -0
- package/src/retrieval/pgvector-retriever.js +420 -0
- package/src/retrieval/qdrant-retriever.js +361 -0
- package/src/security/validator.js +8 -0
- package/src/testing.js +2 -0
- package/src/utils/batch-processor.js +7 -3
- package/src/utils/error-formatter.js +48 -20
- package/src/utils/logger.js +31 -1
- package/src/utils/rate-limiter.js +9 -4
- 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
|
-
/**
|
|
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
|