@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
|
@@ -15,6 +15,7 @@ class Semaphore {
|
|
|
15
15
|
this.waitQueue = [];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Resolves when a slot is free and takes it. */
|
|
18
19
|
async acquire() {
|
|
19
20
|
if (this.currentCount < this.maxConcurrency) {
|
|
20
21
|
this.currentCount++;
|
|
@@ -26,6 +27,7 @@ class Semaphore {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/** Frees a slot, starting the next waiter if there is one. */
|
|
29
31
|
release() {
|
|
30
32
|
this.currentCount--;
|
|
31
33
|
if (this.waitQueue.length > 0) {
|
|
@@ -627,7 +627,11 @@ class PluginRegistry {
|
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
-
|
|
630
|
+
/**
|
|
631
|
+
* The shared plugin registry: maps (type, name) to plugins, checking each
|
|
632
|
+
* against its contract. Signed manifests are required when NODE_ENV is
|
|
633
|
+
* production.
|
|
634
|
+
*/
|
|
631
635
|
const registry = new PluginRegistry({
|
|
632
636
|
verifySignatures: process.env.NODE_ENV === 'production',
|
|
633
637
|
failClosed: process.env.NODE_ENV === 'production',
|
package/src/dag/dag-engine.js
CHANGED
|
@@ -26,6 +26,14 @@ class DAG {
|
|
|
26
26
|
this.errorContext = new ErrorContext();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Adds a node. A source node receives the seed; a node with one input
|
|
31
|
+
* receives that node's result, and with several, an array of them.
|
|
32
|
+
* @param {string} id - Unique node id
|
|
33
|
+
* @param {function(*): *} fn - The node's work; may be async
|
|
34
|
+
* @param {Object} [options]
|
|
35
|
+
* @returns {DAGNode} The node; set `optional` or `retry` on it to change how failures are handled
|
|
36
|
+
*/
|
|
29
37
|
addNode(id, _fn, options = {}) {
|
|
30
38
|
if (this.nodes.has(id)) {
|
|
31
39
|
throw new Error(`Node with ID "${id}" already exists`);
|
|
@@ -35,6 +43,11 @@ class DAG {
|
|
|
35
43
|
return node;
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Adds an edge: `toId` runs after `fromId` and receives its result.
|
|
48
|
+
* @param {string} fromId
|
|
49
|
+
* @param {string} toId
|
|
50
|
+
*/
|
|
38
51
|
connect(fromId, toId) {
|
|
39
52
|
const from = this.nodes.get(fromId);
|
|
40
53
|
const to = this.nodes.get(toId);
|
package/src/dev/dev-server.js
CHANGED
|
@@ -411,6 +411,12 @@ ${this.options.watchPaths.map((p) => `║ - ${p.padEnd(56)}║`).join('\n')}
|
|
|
411
411
|
res.end(JSON.stringify(payload));
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Reloads a plugin or the config now, as if the file had changed.
|
|
416
|
+
* @param {string} filePath
|
|
417
|
+
* @param {'plugin'|'config'} [type='plugin']
|
|
418
|
+
* @returns {Promise<void>}
|
|
419
|
+
*/
|
|
414
420
|
async triggerReload(filePath, type = 'plugin') {
|
|
415
421
|
this._log(`Manually triggering reload: ${filePath}`, 'info');
|
|
416
422
|
await this.hotReload.triggerReload(filePath, type);
|
|
@@ -49,6 +49,11 @@ function evpBytesToKey(password, keyLen, ivLen) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Records authentication, data access, data change and other audit events
|
|
54
|
+
* in hash-chained logs, with querying, compliance reports and an integrity
|
|
55
|
+
* check.
|
|
56
|
+
*/
|
|
52
57
|
class AuditLogger extends EventEmitter {
|
|
53
58
|
constructor(_options = {}) {
|
|
54
59
|
super();
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs an evaluation dataset through a pipeline (runEvaluation) and compares
|
|
5
|
+
* two runs (compareConfigurations). A run's report is plain JSON, so a saved
|
|
6
|
+
* report can stand in for a pipeline: CI compares a pull request against the
|
|
7
|
+
* report saved from its base branch instead of running the base branch.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { validateDataset } = require('./dataset');
|
|
11
|
+
const { version: LIBRARY_VERSION } = require('../../package.json');
|
|
12
|
+
|
|
13
|
+
const REPORT_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
/** Metrics a report can hold; higher is better for all of them. */
|
|
16
|
+
const METRICS = [
|
|
17
|
+
'faithfulness',
|
|
18
|
+
'relevance',
|
|
19
|
+
'contextPrecision',
|
|
20
|
+
'contextRecall',
|
|
21
|
+
'groundedness',
|
|
22
|
+
'citationCoverage',
|
|
23
|
+
'sourceRecall',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
class DatasetError extends Error {
|
|
27
|
+
constructor(errors) {
|
|
28
|
+
const shown = errors.slice(0, 5).map((e) => e.message);
|
|
29
|
+
const more = errors.length > 5 ? ` (and ${errors.length - 5} more)` : '';
|
|
30
|
+
super(`Invalid dataset: ${shown.join('; ')}${more}`);
|
|
31
|
+
this.name = 'DatasetError';
|
|
32
|
+
this.code = 'RAG_INVALID_DATASET';
|
|
33
|
+
this.errors = errors;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isFiniteNumber = (value) =>
|
|
38
|
+
typeof value === 'number' && Number.isFinite(value);
|
|
39
|
+
|
|
40
|
+
const round = (value) => Math.round(value * 10000) / 10000;
|
|
41
|
+
|
|
42
|
+
function scoreCase(testCase, result) {
|
|
43
|
+
const scores = {};
|
|
44
|
+
const evaluation = result.evaluation && result.evaluation.scores;
|
|
45
|
+
if (typeof result.answer === 'string' && evaluation) {
|
|
46
|
+
for (const name of METRICS) {
|
|
47
|
+
if (isFiniteNumber(evaluation[name])) scores[name] = evaluation[name];
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
result.citations &&
|
|
51
|
+
isFiniteNumber(result.citations.groundednessScore)
|
|
52
|
+
) {
|
|
53
|
+
scores.citationCoverage = result.citations.groundednessScore;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (testCase.sourceIds) {
|
|
57
|
+
// A source id matches a result's id or its metadata.source, so a case
|
|
58
|
+
// can name a file ("docs/vacation.md") when the index stores chunks
|
|
59
|
+
// ("docs/vacation.md#0").
|
|
60
|
+
const retrieved = new Set();
|
|
61
|
+
for (const doc of result.results || []) {
|
|
62
|
+
if (!doc) continue;
|
|
63
|
+
if (doc.id !== undefined) retrieved.add(String(doc.id));
|
|
64
|
+
if (doc.metadata && doc.metadata.source !== undefined) {
|
|
65
|
+
retrieved.add(String(doc.metadata.source));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const found = testCase.sourceIds.filter((id) => retrieved.has(id));
|
|
69
|
+
scores.sourceRecall = found.length / testCase.sourceIds.length;
|
|
70
|
+
}
|
|
71
|
+
return scores;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function summarise(cases) {
|
|
75
|
+
const metrics = {};
|
|
76
|
+
for (const name of METRICS) {
|
|
77
|
+
const values = cases
|
|
78
|
+
.filter((c) => c.success && isFiniteNumber(c.scores[name]))
|
|
79
|
+
.map((c) => c.scores[name]);
|
|
80
|
+
if (values.length > 0) {
|
|
81
|
+
metrics[name] = {
|
|
82
|
+
mean: round(values.reduce((a, b) => a + b, 0) / values.length),
|
|
83
|
+
count: values.length,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const failed = cases.filter((c) => !c.success).length;
|
|
88
|
+
return {
|
|
89
|
+
cases: cases.length,
|
|
90
|
+
succeeded: cases.length - failed,
|
|
91
|
+
failed,
|
|
92
|
+
metrics,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function checkedCases(dataset) {
|
|
97
|
+
const report = validateDataset(dataset);
|
|
98
|
+
if (!report.valid) throw new DatasetError(report.errors);
|
|
99
|
+
return report.cases;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Runs every case of a dataset through a pipeline and scores it.
|
|
104
|
+
*
|
|
105
|
+
* Each case runs with citations and evaluation on, and its expected answer
|
|
106
|
+
* as the reference. A case that fails is recorded with its error, not
|
|
107
|
+
* thrown. Throws a DatasetError (`code: 'RAG_INVALID_DATASET'`, `errors`
|
|
108
|
+
* from validateDataset) when the dataset is invalid.
|
|
109
|
+
*
|
|
110
|
+
* @param {Array<Object>|{cases: Array<Object>}} dataset - See validateDataset
|
|
111
|
+
* @param {{ run: Function }} pipeline - A createRagPipeline() pipeline
|
|
112
|
+
* @param {Object} [options]
|
|
113
|
+
* @param {number} [options.concurrency=1] - Cases run at the same time
|
|
114
|
+
* @param {number} [options.timeout] - Per-case timeout in milliseconds
|
|
115
|
+
* @param {AbortSignal} [options.signal] - Stops the run; unstarted cases are not run
|
|
116
|
+
* @param {Object} [options.runOptions] - Extra `run()` options for every case
|
|
117
|
+
* @param {(caseResult: Object, index: number) => void} [options.onCase] - Called as each case finishes
|
|
118
|
+
* @returns {Promise<{
|
|
119
|
+
* version: number,
|
|
120
|
+
* libraryVersion: string,
|
|
121
|
+
* createdAt: string,
|
|
122
|
+
* summary: { cases: number, succeeded: number, failed: number, metrics: Object<string, { mean: number, count: number }> },
|
|
123
|
+
* cases: Array<{ id: string, prompt: string, success: boolean, error?: string, answer: string|null, retrievedIds: string[], scores: Object<string, number>, durationMs: number }>
|
|
124
|
+
* }>} A JSON-serialisable report
|
|
125
|
+
*/
|
|
126
|
+
async function runEvaluation(dataset, pipeline, options = {}) {
|
|
127
|
+
if (!pipeline || typeof pipeline.run !== 'function') {
|
|
128
|
+
throw new TypeError('runEvaluation needs a pipeline with a run() method');
|
|
129
|
+
}
|
|
130
|
+
const { concurrency = 1, timeout, signal, runOptions = {}, onCase } = options;
|
|
131
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
132
|
+
throw new TypeError('concurrency must be a positive integer');
|
|
133
|
+
}
|
|
134
|
+
const testCases = checkedCases(dataset);
|
|
135
|
+
const results = new Array(testCases.length);
|
|
136
|
+
|
|
137
|
+
const runCase = async (testCase, index) => {
|
|
138
|
+
const started = Date.now();
|
|
139
|
+
const record = {
|
|
140
|
+
id: testCase.id,
|
|
141
|
+
prompt: testCase.prompt,
|
|
142
|
+
...(testCase.expected !== undefined && { expected: testCase.expected }),
|
|
143
|
+
...(testCase.sourceIds && { sourceIds: testCase.sourceIds }),
|
|
144
|
+
...(testCase.tags && { tags: testCase.tags }),
|
|
145
|
+
};
|
|
146
|
+
let result;
|
|
147
|
+
try {
|
|
148
|
+
result = await pipeline.run({
|
|
149
|
+
query: testCase.prompt,
|
|
150
|
+
options: {
|
|
151
|
+
...runOptions,
|
|
152
|
+
citations: true,
|
|
153
|
+
evaluate: true,
|
|
154
|
+
...(testCase.expected !== undefined && {
|
|
155
|
+
referenceAnswer: testCase.expected,
|
|
156
|
+
}),
|
|
157
|
+
...(timeout !== undefined && { timeout }),
|
|
158
|
+
...(signal && { signal }),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
} catch (err) {
|
|
162
|
+
result = { success: false, error: err && err.message };
|
|
163
|
+
}
|
|
164
|
+
const success = Boolean(result && result.success);
|
|
165
|
+
Object.assign(record, {
|
|
166
|
+
success,
|
|
167
|
+
...(!success && {
|
|
168
|
+
error: String((result && result.error) || 'Run failed'),
|
|
169
|
+
}),
|
|
170
|
+
answer:
|
|
171
|
+
success && typeof result.answer === 'string' ? result.answer : null,
|
|
172
|
+
retrievedIds: success
|
|
173
|
+
? (result.results || []).map((doc) => String(doc && doc.id))
|
|
174
|
+
: [],
|
|
175
|
+
scores: success ? scoreCase(testCase, result) : {},
|
|
176
|
+
durationMs: Date.now() - started,
|
|
177
|
+
});
|
|
178
|
+
results[index] = record;
|
|
179
|
+
if (onCase) onCase(record, index);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let next = 0;
|
|
183
|
+
const worker = async () => {
|
|
184
|
+
while (next < testCases.length) {
|
|
185
|
+
if (signal && signal.aborted) return;
|
|
186
|
+
const index = next;
|
|
187
|
+
next += 1;
|
|
188
|
+
await runCase(testCases[index], index);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
await Promise.all(
|
|
192
|
+
Array.from({ length: Math.min(concurrency, testCases.length) }, worker),
|
|
193
|
+
);
|
|
194
|
+
if (signal && signal.aborted) {
|
|
195
|
+
const err = new Error('Evaluation aborted');
|
|
196
|
+
err.code = 'ABORT_ERR';
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
version: REPORT_VERSION,
|
|
202
|
+
// Scores can change between library versions (2.5.0 changed grounding),
|
|
203
|
+
// so a comparison warns when two reports come from different ones.
|
|
204
|
+
libraryVersion: LIBRARY_VERSION,
|
|
205
|
+
createdAt: new Date().toISOString(),
|
|
206
|
+
summary: summarise(results),
|
|
207
|
+
cases: results,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isReport(value) {
|
|
212
|
+
return (
|
|
213
|
+
value &&
|
|
214
|
+
typeof value === 'object' &&
|
|
215
|
+
typeof value.run !== 'function' &&
|
|
216
|
+
Array.isArray(value.cases) &&
|
|
217
|
+
value.summary &&
|
|
218
|
+
typeof value.summary === 'object'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function toReport(side, name, dataset, runOptions) {
|
|
223
|
+
if (side && typeof side.run === 'function') {
|
|
224
|
+
if (dataset === undefined || dataset === null) {
|
|
225
|
+
throw new TypeError(`${name} is a pipeline, so a dataset is required`);
|
|
226
|
+
}
|
|
227
|
+
return runEvaluation(dataset, side, runOptions);
|
|
228
|
+
}
|
|
229
|
+
if (isReport(side)) {
|
|
230
|
+
if (side.version !== REPORT_VERSION) {
|
|
231
|
+
throw new TypeError(
|
|
232
|
+
`${name} is a report of version ${side.version}; expected ${REPORT_VERSION}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return side;
|
|
236
|
+
}
|
|
237
|
+
throw new TypeError(
|
|
238
|
+
`${name} must be a pipeline or a report from runEvaluation()`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Compares two configurations on the same dataset and reports what got
|
|
244
|
+
* worse. Each side is a pipeline (run now) or a saved runEvaluation()
|
|
245
|
+
* report, so CI can compare against the base branch's saved report.
|
|
246
|
+
*
|
|
247
|
+
* A metric has regressed when its mean drops by more than `tolerance`.
|
|
248
|
+
* A case has regressed when it now fails, or a metric drops by more than
|
|
249
|
+
* `caseTolerance`; those cases are listed but don't set `regressed` on
|
|
250
|
+
* their own, except failures. Scores from LLM answers vary between runs,
|
|
251
|
+
* so keep the tolerances above the noise you see on repeated runs.
|
|
252
|
+
*
|
|
253
|
+
* @param {Array<Object>|{cases: Array<Object>}|null} dataset - Needed when either side is a pipeline
|
|
254
|
+
* @param {Object} options
|
|
255
|
+
* @param {Object} options.baseline - Pipeline or report
|
|
256
|
+
* @param {Object} options.candidate - Pipeline or report
|
|
257
|
+
* @param {number} [options.tolerance=0.02] - Allowed drop in a metric's mean
|
|
258
|
+
* @param {number} [options.caseTolerance=0.1] - Drop in one case's metric that lists the case
|
|
259
|
+
* @param {string[]} [options.metrics] - Metrics to compare (default: all both runs have)
|
|
260
|
+
* @param {Object} [options.runOptions] - Options for runEvaluation when a side is a pipeline
|
|
261
|
+
* @returns {Promise<{
|
|
262
|
+
* regressed: boolean,
|
|
263
|
+
* metrics: Object<string, { baseline: number, candidate: number, delta: number, regressed: boolean }>,
|
|
264
|
+
* regressions: Array<{ id: string, prompt: string, reason: string, metric?: string, baseline?: number, candidate?: number, delta?: number }>,
|
|
265
|
+
* improvements: Array<{ id: string, prompt: string, reason: string, metric?: string, baseline?: number, candidate?: number, delta?: number }>,
|
|
266
|
+
* warnings: string[],
|
|
267
|
+
* baseline: Object,
|
|
268
|
+
* candidate: Object
|
|
269
|
+
* }>}
|
|
270
|
+
*/
|
|
271
|
+
async function compareConfigurations(dataset, options = {}) {
|
|
272
|
+
const {
|
|
273
|
+
baseline,
|
|
274
|
+
candidate,
|
|
275
|
+
tolerance = 0.02,
|
|
276
|
+
caseTolerance = 0.1,
|
|
277
|
+
metrics: only,
|
|
278
|
+
runOptions,
|
|
279
|
+
} = options;
|
|
280
|
+
for (const [name, value] of [
|
|
281
|
+
['tolerance', tolerance],
|
|
282
|
+
['caseTolerance', caseTolerance],
|
|
283
|
+
]) {
|
|
284
|
+
if (!isFiniteNumber(value) || value < 0) {
|
|
285
|
+
throw new TypeError(`${name} must be a number of 0 or more`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const base = await toReport(baseline, 'baseline', dataset, runOptions);
|
|
289
|
+
const cand = await toReport(candidate, 'candidate', dataset, runOptions);
|
|
290
|
+
const warnings = [];
|
|
291
|
+
if (
|
|
292
|
+
base.libraryVersion &&
|
|
293
|
+
cand.libraryVersion &&
|
|
294
|
+
base.libraryVersion !== cand.libraryVersion
|
|
295
|
+
) {
|
|
296
|
+
warnings.push(
|
|
297
|
+
`The runs used different library versions (${base.libraryVersion} and ${cand.libraryVersion}); scores may differ because of that`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const names = (only || METRICS).filter(
|
|
302
|
+
(name) =>
|
|
303
|
+
base.summary.metrics &&
|
|
304
|
+
cand.summary.metrics &&
|
|
305
|
+
base.summary.metrics[name] &&
|
|
306
|
+
cand.summary.metrics[name],
|
|
307
|
+
);
|
|
308
|
+
if (only) {
|
|
309
|
+
for (const name of only) {
|
|
310
|
+
if (!names.includes(name)) {
|
|
311
|
+
warnings.push(`Metric "${name}" is missing from one of the runs`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const metrics = {};
|
|
317
|
+
for (const name of names) {
|
|
318
|
+
const b = base.summary.metrics[name].mean;
|
|
319
|
+
const c = cand.summary.metrics[name].mean;
|
|
320
|
+
const delta = round(c - b);
|
|
321
|
+
metrics[name] = {
|
|
322
|
+
baseline: b,
|
|
323
|
+
candidate: c,
|
|
324
|
+
delta,
|
|
325
|
+
regressed: delta < -tolerance,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const regressions = [];
|
|
330
|
+
const improvements = [];
|
|
331
|
+
const baseById = new Map(base.cases.map((c) => [c.id, c]));
|
|
332
|
+
const candIds = new Set(cand.cases.map((c) => c.id));
|
|
333
|
+
for (const c of cand.cases) {
|
|
334
|
+
const b = baseById.get(c.id);
|
|
335
|
+
if (!b) {
|
|
336
|
+
warnings.push(`Case "${c.id}" is only in the candidate run`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (b.prompt !== c.prompt) {
|
|
340
|
+
warnings.push(`Case "${c.id}" has a different prompt in the two runs`);
|
|
341
|
+
}
|
|
342
|
+
const entry = { id: c.id, prompt: c.prompt };
|
|
343
|
+
if (b.success && !c.success) {
|
|
344
|
+
regressions.push({ ...entry, reason: `now fails: ${c.error}` });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!b.success && c.success) {
|
|
348
|
+
improvements.push({ ...entry, reason: 'no longer fails' });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (!c.success) continue;
|
|
352
|
+
for (const name of names) {
|
|
353
|
+
if (!isFiniteNumber(b.scores[name]) || !isFiniteNumber(c.scores[name])) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const delta = round(c.scores[name] - b.scores[name]);
|
|
357
|
+
const change = {
|
|
358
|
+
...entry,
|
|
359
|
+
metric: name,
|
|
360
|
+
baseline: b.scores[name],
|
|
361
|
+
candidate: c.scores[name],
|
|
362
|
+
delta,
|
|
363
|
+
};
|
|
364
|
+
if (delta < -caseTolerance) {
|
|
365
|
+
regressions.push({ ...change, reason: `${name} dropped` });
|
|
366
|
+
} else if (delta > caseTolerance) {
|
|
367
|
+
improvements.push({ ...change, reason: `${name} rose` });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const b of base.cases) {
|
|
372
|
+
if (!candIds.has(b.id)) {
|
|
373
|
+
warnings.push(`Case "${b.id}" is only in the baseline run`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const newFailures = regressions.some((r) => r.reason.startsWith('now fails'));
|
|
378
|
+
return {
|
|
379
|
+
regressed:
|
|
380
|
+
newFailures || Object.values(metrics).some((metric) => metric.regressed),
|
|
381
|
+
metrics,
|
|
382
|
+
regressions,
|
|
383
|
+
improvements,
|
|
384
|
+
warnings,
|
|
385
|
+
baseline: base,
|
|
386
|
+
candidate: cand,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
module.exports = {
|
|
391
|
+
runEvaluation,
|
|
392
|
+
compareConfigurations,
|
|
393
|
+
DatasetError,
|
|
394
|
+
EVALUATION_METRICS: METRICS,
|
|
395
|
+
};
|