@devilsdev/rag-pipeline-utils 2.6.2 → 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 +121 -0
- package/README.md +35 -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 +12 -2
- 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,361 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retriever on Qdrant, through its REST API with the built-in fetch: no
|
|
5
|
+
* client package is needed.
|
|
6
|
+
*
|
|
7
|
+
* Qdrant point ids must be integers or UUIDs, so each document id is
|
|
8
|
+
* mapped to a UUID derived from it, and the original id is kept in the
|
|
9
|
+
* payload (`rag_id`) with `content` and `metadata`. Writes wait for Qdrant
|
|
10
|
+
* to apply them (`wait=true`), so a query right after ingest sees them.
|
|
11
|
+
* Written against the Qdrant 1.20 API (`/points/query`).
|
|
12
|
+
*
|
|
13
|
+
* The collection can change without this process knowing, so the
|
|
14
|
+
* retriever has no cache identity unless you give it a `cacheKey`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { throwIfAborted } = require('../connectors/connector-utils');
|
|
19
|
+
|
|
20
|
+
const COLLECTION = /^[A-Za-z0-9_-]{1,255}$/;
|
|
21
|
+
const DEFAULT_COLLECTION = 'rag_documents';
|
|
22
|
+
const PAGE = 256;
|
|
23
|
+
const UPSERT_BATCH = 256;
|
|
24
|
+
|
|
25
|
+
/** A stable UUID (version 8, RFC 9562) for a document id. */
|
|
26
|
+
function pointId(id) {
|
|
27
|
+
const h = crypto.createHash('sha256').update(String(id)).digest();
|
|
28
|
+
h[6] = (h[6] & 0x0f) | 0x80;
|
|
29
|
+
h[8] = (h[8] & 0x3f) | 0x80;
|
|
30
|
+
const x = h.subarray(0, 16).toString('hex');
|
|
31
|
+
return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkVector(vector, id) {
|
|
35
|
+
if (!Array.isArray(vector) || vector.length === 0) {
|
|
36
|
+
throw new Error(`QdrantRetriever: document "${id}" has no vector`);
|
|
37
|
+
}
|
|
38
|
+
if (!vector.every((x) => typeof x === 'number' && Number.isFinite(x))) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`QdrantRetriever: document "${id}" has a vector with a non-finite value`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Matches every point this class wrote, and nothing else in the collection.
|
|
46
|
+
const OURS = { must_not: [{ is_empty: { key: 'rag_id' } }] };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Retriever on Qdrant over its REST API, with no client package. Creates
|
|
50
|
+
* the collection on first store; similarity is cosine.
|
|
51
|
+
*/
|
|
52
|
+
class QdrantRetriever {
|
|
53
|
+
/**
|
|
54
|
+
* @param {Object} [options]
|
|
55
|
+
* @param {string} [options.url='http://localhost:6333'] - Qdrant's REST endpoint
|
|
56
|
+
* @param {string} [options.apiKey] - Sent as the `api-key` header
|
|
57
|
+
* @param {string} [options.collection='rag_documents']
|
|
58
|
+
* @param {number} [options.dimensions] - Vector size for a new collection; defaults to the first stored vector's
|
|
59
|
+
* @param {boolean} [options.createCollection=true] - Create the collection on first store
|
|
60
|
+
* @param {string} [options.cacheKey] - Cache identity; change it when the collection changes
|
|
61
|
+
* @param {Function} [options.fetch] - fetch implementation (defaults to the global)
|
|
62
|
+
*/
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
const {
|
|
65
|
+
url = 'http://localhost:6333',
|
|
66
|
+
apiKey,
|
|
67
|
+
collection = DEFAULT_COLLECTION,
|
|
68
|
+
dimensions,
|
|
69
|
+
createCollection = true,
|
|
70
|
+
cacheKey,
|
|
71
|
+
fetch: fetchImpl = globalThis.fetch,
|
|
72
|
+
} = options;
|
|
73
|
+
if (!COLLECTION.test(collection)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`QdrantRetriever: invalid collection name "${collection}" (letters, digits, "_" and "-")`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
dimensions !== undefined &&
|
|
80
|
+
!(Number.isInteger(dimensions) && dimensions > 0)
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'QdrantRetriever: `dimensions` must be a positive integer',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (typeof fetchImpl !== 'function') {
|
|
87
|
+
throw new Error('QdrantRetriever needs fetch (Node.js 18 or later)');
|
|
88
|
+
}
|
|
89
|
+
this.url = String(url).replace(/\/+$/, '');
|
|
90
|
+
this.collection = collection;
|
|
91
|
+
this.dimensions = dimensions;
|
|
92
|
+
this.createCollection = createCollection;
|
|
93
|
+
this.cacheKey = cacheKey;
|
|
94
|
+
this._apiKey = apiKey;
|
|
95
|
+
this._fetch = fetchImpl;
|
|
96
|
+
this._ready = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Cache identity for createRagPipeline({ cache }): none unless you set
|
|
101
|
+
* `cacheKey`, since other clients can change the collection.
|
|
102
|
+
* @param {'retriever'} role
|
|
103
|
+
* @returns {string|null}
|
|
104
|
+
*/
|
|
105
|
+
getCacheKey(role) {
|
|
106
|
+
if (role !== 'retriever' || !this.cacheKey) return null;
|
|
107
|
+
return `qdrant|${this.url}|${this.collection}|${this.cacheKey}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* One REST call to the collection; resolves Qdrant's `result`.
|
|
112
|
+
* @private
|
|
113
|
+
* @param {string} method
|
|
114
|
+
* @param {string} path - After /collections/<name>
|
|
115
|
+
* @param {Object} [body]
|
|
116
|
+
* @param {{ signal?: AbortSignal, allow404?: boolean }} [options] - allow404 resolves null for a missing collection
|
|
117
|
+
* @returns {Promise<any>}
|
|
118
|
+
*/
|
|
119
|
+
async _request(method, path, body, { signal, allow404 = false } = {}) {
|
|
120
|
+
throwIfAborted(signal);
|
|
121
|
+
const headers = {
|
|
122
|
+
'content-type': 'application/json',
|
|
123
|
+
// fetch asks for gzip by default, and Qdrant's compressed responses
|
|
124
|
+
// stall each request by about 40 ms; uncompressed they take ~1 ms.
|
|
125
|
+
'accept-encoding': 'identity',
|
|
126
|
+
};
|
|
127
|
+
if (this._apiKey) headers['api-key'] = this._apiKey;
|
|
128
|
+
let res;
|
|
129
|
+
try {
|
|
130
|
+
res = await this._fetch(
|
|
131
|
+
`${this.url}/collections/${this.collection}${path}`,
|
|
132
|
+
{
|
|
133
|
+
method,
|
|
134
|
+
headers,
|
|
135
|
+
...(body !== undefined && { body: JSON.stringify(body) }),
|
|
136
|
+
...(signal && { signal }),
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throwIfAborted(signal);
|
|
141
|
+
throw new Error(
|
|
142
|
+
`QdrantRetriever: cannot reach Qdrant at ${this.url} (${err.message})`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (res.status === 404 && allow404) return null;
|
|
146
|
+
const text = await res.text();
|
|
147
|
+
let json;
|
|
148
|
+
try {
|
|
149
|
+
json = text ? JSON.parse(text) : {};
|
|
150
|
+
} catch {
|
|
151
|
+
json = {};
|
|
152
|
+
}
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
const detail =
|
|
155
|
+
(json.status && json.status.error) || text || res.statusText;
|
|
156
|
+
if (res.status === 401 || res.status === 403) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`QdrantRetriever: Qdrant refused the request (${res.status}); check QDRANT_API_KEY`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
throw new Error(
|
|
162
|
+
`QdrantRetriever: ${method} ${path || '/'} failed (${res.status}): ${detail}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return json.result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_ensureCollection(dimensions) {
|
|
169
|
+
if (!this.createCollection) return Promise.resolve();
|
|
170
|
+
if (!this._ready) {
|
|
171
|
+
this._ready = (async () => {
|
|
172
|
+
const existing = await this._request('GET', '', undefined, {
|
|
173
|
+
allow404: true,
|
|
174
|
+
});
|
|
175
|
+
if (!existing) {
|
|
176
|
+
await this._request('PUT', '', {
|
|
177
|
+
vectors: {
|
|
178
|
+
size: this.dimensions || dimensions,
|
|
179
|
+
distance: 'Cosine',
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// removeBySource filters on metadata.source; index it.
|
|
184
|
+
await this._request('PUT', '/index?wait=true', {
|
|
185
|
+
field_name: 'metadata.source',
|
|
186
|
+
field_schema: 'keyword',
|
|
187
|
+
});
|
|
188
|
+
})().catch((err) => {
|
|
189
|
+
this._ready = null;
|
|
190
|
+
throw err;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return this._ready;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Inserts or replaces documents.
|
|
198
|
+
* @param {Array<{ id: string, vector: number[], content?: string, metadata?: Object }>} docs
|
|
199
|
+
* @returns {Promise<void>}
|
|
200
|
+
*/
|
|
201
|
+
async store(docs) {
|
|
202
|
+
if (!Array.isArray(docs))
|
|
203
|
+
throw new Error('QdrantRetriever.store() takes an array');
|
|
204
|
+
if (docs.length === 0) return;
|
|
205
|
+
let dimensions = null;
|
|
206
|
+
const points = docs.map((doc) => {
|
|
207
|
+
const id = doc && doc.id;
|
|
208
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
'QdrantRetriever.store(): every document needs a string id',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
checkVector(doc.vector, id);
|
|
214
|
+
if (dimensions === null) dimensions = doc.vector.length;
|
|
215
|
+
else if (doc.vector.length !== dimensions) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`QdrantRetriever.store(): document "${id}" has ${doc.vector.length} dimensions, the batch has ${dimensions}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
id: pointId(id),
|
|
222
|
+
vector: doc.vector,
|
|
223
|
+
payload: {
|
|
224
|
+
rag_id: id,
|
|
225
|
+
content: doc.content || '',
|
|
226
|
+
metadata: doc.metadata || {},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
await this._ensureCollection(dimensions);
|
|
231
|
+
for (let i = 0; i < points.length; i += UPSERT_BATCH) {
|
|
232
|
+
await this._request('PUT', '/points?wait=true', {
|
|
233
|
+
points: points.slice(i, i + UPSERT_BATCH),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The documents most similar to a query vector.
|
|
240
|
+
* @param {number[]|{ queryVector: number[], topK?: number, signal?: AbortSignal }} request
|
|
241
|
+
* @param {number} [k=10] - Results when `request` is a vector
|
|
242
|
+
* @returns {Promise<Array<{ id: string, score: number, content: string, metadata: Object }>>}
|
|
243
|
+
*/
|
|
244
|
+
async retrieve(request, k = 10) {
|
|
245
|
+
const {
|
|
246
|
+
queryVector,
|
|
247
|
+
topK = k,
|
|
248
|
+
signal,
|
|
249
|
+
} = Array.isArray(request) ? { queryVector: request } : request || {};
|
|
250
|
+
if (!Number.isInteger(topK) || topK < 1) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
'QdrantRetriever.retrieve(): topK must be a positive integer',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
checkVector(queryVector, 'query');
|
|
256
|
+
const result = await this._request(
|
|
257
|
+
'POST',
|
|
258
|
+
'/points/query',
|
|
259
|
+
{ query: queryVector, limit: topK, with_payload: true, filter: OURS },
|
|
260
|
+
{ signal, allow404: true },
|
|
261
|
+
);
|
|
262
|
+
throwIfAborted(signal);
|
|
263
|
+
return ((result && result.points) || []).map((p) => ({
|
|
264
|
+
id: p.payload.rag_id,
|
|
265
|
+
score: p.score,
|
|
266
|
+
content: p.payload.content || '',
|
|
267
|
+
metadata: p.payload.metadata || {},
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Deletes the chunks of one source (`metadata.source`), for re-ingesting.
|
|
273
|
+
* @param {string} source
|
|
274
|
+
*/
|
|
275
|
+
async removeBySource(source) {
|
|
276
|
+
await this._request(
|
|
277
|
+
'POST',
|
|
278
|
+
'/points/delete?wait=true',
|
|
279
|
+
{
|
|
280
|
+
filter: {
|
|
281
|
+
must: [{ key: 'metadata.source', match: { value: String(source) } }],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{ allow404: true },
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Deletes documents by id.
|
|
290
|
+
* @param {string[]} ids
|
|
291
|
+
*/
|
|
292
|
+
async delete(ids) {
|
|
293
|
+
await this._request(
|
|
294
|
+
'POST',
|
|
295
|
+
'/points/delete?wait=true',
|
|
296
|
+
{ points: ids.map(pointId) },
|
|
297
|
+
{ allow404: true },
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Deletes every document this class stored; the collection stays. */
|
|
302
|
+
async clear() {
|
|
303
|
+
await this._request(
|
|
304
|
+
'POST',
|
|
305
|
+
'/points/delete?wait=true',
|
|
306
|
+
{ filter: OURS },
|
|
307
|
+
{ allow404: true },
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** @returns {Promise<number>} */
|
|
312
|
+
async size() {
|
|
313
|
+
const result = await this._request(
|
|
314
|
+
'POST',
|
|
315
|
+
'/points/count',
|
|
316
|
+
{ exact: true, filter: OURS },
|
|
317
|
+
{ allow404: true },
|
|
318
|
+
);
|
|
319
|
+
return result ? result.count : 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Every document, without vectors.
|
|
324
|
+
* @param {{ limit?: number }} [options]
|
|
325
|
+
* @returns {Promise<Array<{ id: string, content: string, metadata: Object }>>}
|
|
326
|
+
*/
|
|
327
|
+
async list({ limit } = {}) {
|
|
328
|
+
const out = [];
|
|
329
|
+
let offset;
|
|
330
|
+
do {
|
|
331
|
+
const result = await this._request(
|
|
332
|
+
'POST',
|
|
333
|
+
'/points/scroll',
|
|
334
|
+
{
|
|
335
|
+
limit: limit ? Math.min(PAGE, limit - out.length) : PAGE,
|
|
336
|
+
with_payload: true,
|
|
337
|
+
with_vector: false,
|
|
338
|
+
filter: OURS,
|
|
339
|
+
...(offset !== undefined && offset !== null && { offset }),
|
|
340
|
+
},
|
|
341
|
+
{ allow404: true },
|
|
342
|
+
);
|
|
343
|
+
if (!result) break;
|
|
344
|
+
for (const p of result.points) {
|
|
345
|
+
out.push({
|
|
346
|
+
id: p.payload.rag_id,
|
|
347
|
+
content: p.payload.content || '',
|
|
348
|
+
metadata: p.payload.metadata || {},
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
offset = result.next_page_offset;
|
|
352
|
+
} while (
|
|
353
|
+
offset !== undefined &&
|
|
354
|
+
offset !== null &&
|
|
355
|
+
(!limit || out.length < limit)
|
|
356
|
+
);
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = { QdrantRetriever, pointId };
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Checks input against a maximum length and a fixed list of disallowed
|
|
5
|
+
* tokens (code fences, `${`, `</script`, `--`, `;`, `||`, `&&`).
|
|
6
|
+
* @param {*} input - Converted to a string
|
|
7
|
+
* @param {Object} [opts]
|
|
8
|
+
* @param {number} [opts.maxLength=20000]
|
|
9
|
+
* @returns {{valid: boolean, reasons: string[], sanitized: string}} `sanitized` is the input cut to `maxLength`
|
|
10
|
+
*/
|
|
3
11
|
function validateInput(input, opts = {}) {
|
|
4
12
|
const maxLength = opts.maxLength || 20000;
|
|
5
13
|
const inputStr = String(input);
|
package/src/testing.js
CHANGED
|
@@ -124,7 +124,9 @@ class EchoLLM {
|
|
|
124
124
|
* there are no documents and no `answer`
|
|
125
125
|
*/
|
|
126
126
|
constructor({ answer, fallback = 'No information found.' } = {}) {
|
|
127
|
+
/** The fixed answer, or a function of (query, docs) that returns it. */
|
|
127
128
|
this.answer = answer;
|
|
129
|
+
/** Returned when there are no documents and no `answer`. */
|
|
128
130
|
this.fallback = fallback;
|
|
129
131
|
/** @type {Array<{query: string, docs: Array<Object>, options: Object}>} */
|
|
130
132
|
this.calls = [];
|
|
@@ -111,9 +111,13 @@ class BatchProcessor extends EventEmitter {
|
|
|
111
111
|
/**
|
|
112
112
|
* Process items in optimized batches
|
|
113
113
|
*
|
|
114
|
-
* @param {Array<string|Object>} items - Items to process
|
|
115
|
-
* @param {
|
|
116
|
-
* @param {Object} options - Processing options
|
|
114
|
+
* @param {Array<string|Object>} items - Items to process; objects are read from `text` or `content`
|
|
115
|
+
* @param {function(string[], {batchIndex: number, estimatedTokens: number, signal: AbortSignal}): Promise<Array>} processFn - Processes one batch of texts, returning one result per text
|
|
116
|
+
* @param {Object} [options] - Processing options
|
|
117
|
+
* @param {function(Object): void} [options.onProgress]
|
|
118
|
+
* @param {function(Object): void} [options.onBatchComplete]
|
|
119
|
+
* @param {AbortController} [options.abortController] - Abort to stop between batches
|
|
120
|
+
* @param {boolean} [options.returnOriginalOrder=true]
|
|
117
121
|
* @returns {Promise<Array>} Processed results
|
|
118
122
|
*/
|
|
119
123
|
async processBatches(items, processFn, options = {}) {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Known error codes, grouped by prefix (CONFIG_, PLUGIN_, PIPELINE_,
|
|
15
|
+
* VALIDATION_, FS_, NETWORK_, HOT_RELOAD_, DAG_, AUTH_). Each value equals
|
|
16
|
+
* its key.
|
|
15
17
|
*/
|
|
16
18
|
const ERROR_CODES = {
|
|
17
19
|
// Configuration errors (CONFIG_*)
|
|
@@ -75,7 +77,9 @@ const ERROR_CODES = {
|
|
|
75
77
|
};
|
|
76
78
|
|
|
77
79
|
/**
|
|
78
|
-
*
|
|
80
|
+
* Message, suggestion and documentation templates keyed by error code.
|
|
81
|
+
* Placeholders such as {path} are filled from the context passed to
|
|
82
|
+
* formatError. Codes without an entry fall back to UNKNOWN_ERROR.
|
|
79
83
|
*/
|
|
80
84
|
const ERROR_TEMPLATES = {
|
|
81
85
|
[ERROR_CODES.CONFIG_NOT_FOUND]: {
|
|
@@ -351,9 +355,15 @@ const ERROR_TEMPLATES = {
|
|
|
351
355
|
};
|
|
352
356
|
|
|
353
357
|
/**
|
|
354
|
-
*
|
|
358
|
+
* Error carrying a code, the context used to build its message, suggested
|
|
359
|
+
* fixes and a documentation path, all taken from ERROR_TEMPLATES.
|
|
355
360
|
*/
|
|
356
361
|
class EnhancedError extends Error {
|
|
362
|
+
/**
|
|
363
|
+
* @param {string} code - Error code, normally one of ERROR_CODES
|
|
364
|
+
* @param {Object<string, *>} [context={}] - Values for the template placeholders
|
|
365
|
+
* @param {Error|null} [originalError=null] - Underlying error, if any
|
|
366
|
+
*/
|
|
357
367
|
constructor(code, context = {}, originalError = null) {
|
|
358
368
|
const formattedMessage = formatError(code, context);
|
|
359
369
|
super(formattedMessage.message);
|
|
@@ -373,14 +383,17 @@ class EnhancedError extends Error {
|
|
|
373
383
|
}
|
|
374
384
|
|
|
375
385
|
/**
|
|
376
|
-
*
|
|
386
|
+
* Format this error for display with formatErrorMessage's default options.
|
|
387
|
+
* @returns {string} Multi-line message with code, suggestions and documentation link
|
|
377
388
|
*/
|
|
378
389
|
getFormattedMessage() {
|
|
379
390
|
return formatErrorMessage(this);
|
|
380
391
|
}
|
|
381
392
|
|
|
382
393
|
/**
|
|
383
|
-
*
|
|
394
|
+
* Plain object for JSON logging, including the stack and the original
|
|
395
|
+
* error's message and stack (or null).
|
|
396
|
+
* @returns {{name: string, code: string, message: string, suggestions: string[], documentation: string, context: Object<string, *>, timestamp: string, stack: string, originalError: ({message: string, stack: string}|null)}}
|
|
384
397
|
*/
|
|
385
398
|
toJSON() {
|
|
386
399
|
return {
|
|
@@ -403,11 +416,13 @@ class EnhancedError extends Error {
|
|
|
403
416
|
}
|
|
404
417
|
|
|
405
418
|
/**
|
|
406
|
-
*
|
|
419
|
+
* Fill the template for an error code with context values. Unknown codes use
|
|
420
|
+
* the UNKNOWN_ERROR template. Each placeholder is replaced at its first
|
|
421
|
+
* occurrence only; placeholders with no context value stay as written.
|
|
407
422
|
*
|
|
408
423
|
* @param {string} code - Error code
|
|
409
|
-
* @param {
|
|
410
|
-
* @returns {
|
|
424
|
+
* @param {Object<string, *>} [context={}] - Placeholder values, converted with String()
|
|
425
|
+
* @returns {{code: string, message: string, suggestions: string[], documentation: string, context: Object<string, *>}} Formatted error details
|
|
411
426
|
*/
|
|
412
427
|
function formatError(code, context = {}) {
|
|
413
428
|
const template =
|
|
@@ -438,10 +453,17 @@ function formatError(code, context = {}) {
|
|
|
438
453
|
}
|
|
439
454
|
|
|
440
455
|
/**
|
|
441
|
-
*
|
|
456
|
+
* Build a multi-line display string for an error. Fields the error lacks
|
|
457
|
+
* (code, suggestions, documentation) are skipped.
|
|
442
458
|
*
|
|
443
459
|
* @param {Error|EnhancedError} error - Error to format
|
|
444
|
-
* @param {
|
|
460
|
+
* @param {Object} [options] - Formatting options
|
|
461
|
+
* @param {boolean} [options.includeCode=true] - Prefix the message with the error code
|
|
462
|
+
* @param {boolean} [options.includeSuggestions=true] - List the suggestions
|
|
463
|
+
* @param {boolean} [options.includeDocumentation=true] - Add the documentation URL
|
|
464
|
+
* @param {boolean} [options.includeContext=false] - List the context values as JSON
|
|
465
|
+
* @param {boolean} [options.includeStack=false] - Append the stack trace
|
|
466
|
+
* @param {boolean} [options.color=false] - Accepted but unused
|
|
445
467
|
* @returns {string} Formatted error message
|
|
446
468
|
*/
|
|
447
469
|
function formatErrorMessage(error, options = {}) {
|
|
@@ -500,11 +522,11 @@ function formatErrorMessage(error, options = {}) {
|
|
|
500
522
|
}
|
|
501
523
|
|
|
502
524
|
/**
|
|
503
|
-
* Create an
|
|
525
|
+
* Create an EnhancedError. Same as calling its constructor.
|
|
504
526
|
*
|
|
505
527
|
* @param {string} code - Error code
|
|
506
|
-
* @param {
|
|
507
|
-
* @param {Error} originalError -
|
|
528
|
+
* @param {Object<string, *>} [context={}] - Placeholder values
|
|
529
|
+
* @param {Error|null} [originalError=null] - Underlying error, if any
|
|
508
530
|
* @returns {EnhancedError} Enhanced error instance
|
|
509
531
|
*/
|
|
510
532
|
function createError(code, context = {}, originalError = null) {
|
|
@@ -512,11 +534,12 @@ function createError(code, context = {}, originalError = null) {
|
|
|
512
534
|
}
|
|
513
535
|
|
|
514
536
|
/**
|
|
515
|
-
* Wrap an existing error
|
|
537
|
+
* Wrap an existing error in an EnhancedError. The original message is added
|
|
538
|
+
* to the context as originalMessage and the error is kept as originalError.
|
|
516
539
|
*
|
|
517
540
|
* @param {Error} error - Original error
|
|
518
541
|
* @param {string} code - Error code
|
|
519
|
-
* @param {
|
|
542
|
+
* @param {Object<string, *>} [context={}] - Additional placeholder values
|
|
520
543
|
* @returns {EnhancedError} Enhanced error
|
|
521
544
|
*/
|
|
522
545
|
function wrapError(error, code, context = {}) {
|
|
@@ -531,7 +554,7 @@ function wrapError(error, code, context = {}) {
|
|
|
531
554
|
}
|
|
532
555
|
|
|
533
556
|
/**
|
|
534
|
-
* Check
|
|
557
|
+
* Check whether a value is an EnhancedError instance.
|
|
535
558
|
*
|
|
536
559
|
* @param {*} error - Value to check
|
|
537
560
|
* @returns {boolean} True if enhanced error
|
|
@@ -541,10 +564,12 @@ function isEnhancedError(error) {
|
|
|
541
564
|
}
|
|
542
565
|
|
|
543
566
|
/**
|
|
544
|
-
*
|
|
567
|
+
* Severity for an error code: 'critical' for PIPELINE_CREATION_FAILED,
|
|
568
|
+
* CONFIG_NOT_FOUND, FS_PERMISSION_DENIED and AUTH_INVALID_TOKEN, 'warning'
|
|
569
|
+
* for the HOT_RELOAD_FAILED and HOT_RELOAD_STATE_ERROR codes, 'error' otherwise.
|
|
545
570
|
*
|
|
546
571
|
* @param {string} code - Error code
|
|
547
|
-
* @returns {
|
|
572
|
+
* @returns {'critical'|'warning'|'error'} Severity level
|
|
548
573
|
*/
|
|
549
574
|
function getErrorSeverity(code) {
|
|
550
575
|
const critical = [
|
|
@@ -565,10 +590,13 @@ function getErrorSeverity(code) {
|
|
|
565
590
|
}
|
|
566
591
|
|
|
567
592
|
/**
|
|
568
|
-
*
|
|
593
|
+
* Format an error and write it to a logger. Warnings go to logger.warn,
|
|
594
|
+
* everything else to logger.error; critical errors include the stack trace.
|
|
595
|
+
* Errors that are not EnhancedError are treated as 'error'.
|
|
569
596
|
*
|
|
570
597
|
* @param {Error|EnhancedError} error - Error to log
|
|
571
|
-
* @param {
|
|
598
|
+
* @param {Pick<Console, 'error'|'warn'>} [logger=console] - Logger to write to
|
|
599
|
+
* @returns {void}
|
|
572
600
|
*/
|
|
573
601
|
function logError(error, logger = console) {
|
|
574
602
|
const severity = isEnhancedError(error)
|
package/src/utils/logger.js
CHANGED
|
@@ -154,18 +154,38 @@ class StructuredLogger {
|
|
|
154
154
|
this.logger[level](logData, message);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Logs at info level.
|
|
159
|
+
* @param {string} message
|
|
160
|
+
* @param {*} [meta] - An object, Error or value to attach
|
|
161
|
+
*/
|
|
157
162
|
info(message, meta) {
|
|
158
163
|
this._log('info', message, meta);
|
|
159
164
|
}
|
|
160
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Logs at error level.
|
|
168
|
+
* @param {string} message
|
|
169
|
+
* @param {*} [meta] - An object, Error or value to attach
|
|
170
|
+
*/
|
|
161
171
|
error(message, meta) {
|
|
162
172
|
this._log('error', message, meta);
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Logs at warn level.
|
|
177
|
+
* @param {string} message
|
|
178
|
+
* @param {*} [meta] - An object, Error or value to attach
|
|
179
|
+
*/
|
|
165
180
|
warn(message, meta) {
|
|
166
181
|
this._log('warn', message, meta);
|
|
167
182
|
}
|
|
168
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Logs at debug level.
|
|
186
|
+
* @param {string} message
|
|
187
|
+
* @param {*} [meta] - An object, Error or value to attach
|
|
188
|
+
*/
|
|
169
189
|
debug(message, meta) {
|
|
170
190
|
this._log('debug', message, meta);
|
|
171
191
|
}
|
|
@@ -207,7 +227,7 @@ class StructuredLogger {
|
|
|
207
227
|
}
|
|
208
228
|
}
|
|
209
229
|
|
|
210
|
-
|
|
230
|
+
/** The shared structured logger (pino), exported as `logger.logger`. */
|
|
211
231
|
const logger = new StructuredLogger(baseLogger);
|
|
212
232
|
|
|
213
233
|
/**
|
|
@@ -238,4 +258,14 @@ module.exports = {
|
|
|
238
258
|
createLogger,
|
|
239
259
|
correlationMiddleware,
|
|
240
260
|
StructuredLogger,
|
|
261
|
+
// The package exports this module as `logger`, so `logger.info()` and the
|
|
262
|
+
// rest log through the shared instance as well as `logger.logger.info()`.
|
|
263
|
+
/** Logs at info level. Same as `logger.logger.info()`. @param {string} message @param {*} [meta] */
|
|
264
|
+
info: (message, meta) => logger.info(message, meta),
|
|
265
|
+
/** Logs at warn level. Same as `logger.logger.warn()`. @param {string} message @param {*} [meta] */
|
|
266
|
+
warn: (message, meta) => logger.warn(message, meta),
|
|
267
|
+
/** Logs at error level. Same as `logger.logger.error()`. @param {string} message @param {*} [meta] */
|
|
268
|
+
error: (message, meta) => logger.error(message, meta),
|
|
269
|
+
/** Logs at debug level. Same as `logger.logger.debug()`. @param {string} message @param {*} [meta] */
|
|
270
|
+
debug: (message, meta) => logger.debug(message, meta),
|
|
241
271
|
};
|
|
@@ -11,14 +11,19 @@
|
|
|
11
11
|
|
|
12
12
|
const crypto = require('crypto');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Sliding-window limiter: allows up to `maxAttempts` requests per identifier
|
|
16
|
+
* in `windowMs`, then blocks that identifier for `blockDurationMs`. Check a
|
|
17
|
+
* request with allowRequest(identifier).
|
|
18
|
+
*/
|
|
14
19
|
class RateLimiter {
|
|
15
20
|
/**
|
|
16
21
|
* Create a rate limiter instance
|
|
17
22
|
* @param {Object} options - Configuration options
|
|
18
|
-
* @param {number} options.maxAttempts - Maximum attempts allowed (default: 5)
|
|
19
|
-
* @param {number} options.windowMs - Time window in milliseconds (default: 15 minutes)
|
|
20
|
-
* @param {number} options.blockDurationMs - How long to block after exceeding limit (default: 1 hour)
|
|
21
|
-
* @param {number} options.cleanupIntervalMs - How often to cleanup expired entries (default: 5 minutes)
|
|
23
|
+
* @param {number} [options.maxAttempts=5] - Maximum attempts allowed (default: 5)
|
|
24
|
+
* @param {number} [options.windowMs=900000] - Time window in milliseconds (default: 15 minutes)
|
|
25
|
+
* @param {number} [options.blockDurationMs=3600000] - How long to block after exceeding limit (default: 1 hour)
|
|
26
|
+
* @param {number} [options.cleanupIntervalMs=300000] - How often to cleanup expired entries (default: 5 minutes)
|
|
22
27
|
*/
|
|
23
28
|
constructor(options = {}) {
|
|
24
29
|
this.maxAttempts = options.maxAttempts || 5;
|
package/src/utils/retry.js
CHANGED
|
@@ -75,7 +75,16 @@ async function retry(fn, opts = {}) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Older wrapper around retry(): retries `fn` with exponential backoff from
|
|
80
|
+
* `initialDelay` ms and logs each retry and the final failure under `label`.
|
|
81
|
+
* @param {function(number): *} fn - The operation; called with the attempt index
|
|
82
|
+
* @param {Object} [options]
|
|
83
|
+
* @param {number} [options.retries=3] - Retries after the first attempt
|
|
84
|
+
* @param {number} [options.initialDelay=300] - First backoff delay in ms
|
|
85
|
+
* @param {string} [options.label='operation'] - Name used in log messages
|
|
86
|
+
* @returns {Promise<*>} The operation's result
|
|
87
|
+
*/
|
|
79
88
|
async function withRetry(
|
|
80
89
|
_fn,
|
|
81
90
|
{ retries = 3, initialDelay = 300, label = 'operation' } = {},
|