@devilsdev/rag-pipeline-utils 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +1 -1
- package/dist/index.cjs +1484 -147
- package/dist/index.d.ts +49 -0
- package/dist/index.mjs +1 -1
- package/dist/testing.cjs +2 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.mjs +5 -0
- package/dist/types/adapters/ai-sdk.d.ts +53 -0
- package/dist/types/cache/component-keys.d.ts +7 -0
- package/dist/types/cache/memory-cache.d.ts +54 -0
- package/dist/types/cache/pipeline-cache.d.ts +66 -0
- package/dist/types/cache/redis-cache.d.ts +46 -0
- package/dist/types/connectors/anthropic-connector.d.ts +7 -2
- package/dist/types/connectors/cohere-connector.d.ts +7 -2
- package/dist/types/connectors/local-embedder.d.ts +9 -0
- package/dist/types/connectors/memory-retriever.d.ts +8 -0
- package/dist/types/connectors/ollama-connector.d.ts +7 -2
- package/dist/types/connectors/openai-connector.d.ts +16 -1
- package/dist/types/core/create-pipeline.d.ts +8 -0
- package/dist/types/core/plugin-registry.d.ts +136 -0
- package/dist/types/core/stage-contracts.d.ts +36 -0
- package/dist/types/core/validate-plugin.d.ts +45 -0
- package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
- package/dist/types/grounding/check-answer.d.ts +101 -0
- package/dist/types/retrieval/file-vector-store.d.ts +12 -0
- package/dist/types/testing.d.ts +168 -0
- package/package.json +22 -7
- package/src/adapters/ai-sdk.js +215 -0
- package/src/cache/component-keys.js +38 -0
- package/src/cache/memory-cache.js +87 -0
- package/src/cache/pipeline-cache.js +260 -0
- package/src/cache/redis-cache.js +83 -0
- package/src/cli/commands/ai.js +17 -12
- package/src/cli/commands/plugin-hub.js +18 -13
- package/src/cli/enhanced-cli-commands.js +45 -29
- package/src/cli/handlers/query-handler.js +174 -11
- package/src/cli/services/instrument.js +83 -0
- package/src/cli/services/rag-service.js +51 -12
- package/src/cli/wizard/init-wizard.js +2 -17
- package/src/config/enhanced-ragrc-schema.js +0 -2
- package/src/config/validate-schema.js +4 -1
- package/src/connectors/anthropic-connector.js +22 -2
- package/src/connectors/cohere-connector.js +20 -2
- package/src/connectors/local-embedder.js +20 -0
- package/src/connectors/memory-retriever.js +18 -0
- package/src/connectors/ollama-connector.js +29 -2
- package/src/connectors/openai-connector.js +49 -4
- package/src/core/create-pipeline.js +112 -15
- package/src/core/performance/parallel-processor.js +62 -1
- package/src/core/plugin-registry.js +42 -19
- package/src/core/stage-contracts.js +36 -0
- package/src/core/validate-plugin.js +85 -0
- package/src/debug/execution-tracer.js +14 -4
- package/src/dev/dev-server.js +8 -1
- package/src/dev/hot-reload.js +4 -3
- package/src/enterprise/sso-integration.js +4 -3
- package/src/evaluate/semantic-similarity.js +79 -0
- package/src/grounding/check-answer.js +173 -0
- package/src/index.js +22 -0
- package/src/loader/csv-loader.js +3 -2
- package/src/loader/html-loader.js +3 -1
- package/src/loader/markdown-loader.js +3 -2
- package/src/retrieval/file-vector-store.js +30 -1
- package/src/security/jwt-validator.js +5 -4
- package/src/testing.js +254 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-process cache with least-recently-used eviction and per-entry time to
|
|
5
|
+
* live. Implements the CacheBackend contract:
|
|
6
|
+
*
|
|
7
|
+
* get(key) -> Promise<value | undefined>
|
|
8
|
+
* set(key, value, { ttlSeconds }) -> Promise<void>
|
|
9
|
+
* delete(key) -> Promise<void>
|
|
10
|
+
* stats() -> Promise<{ hits, misses, size }>
|
|
11
|
+
*
|
|
12
|
+
* Values are stored as JSON copies, so later changes to a returned value do
|
|
13
|
+
* not change the cache (the same behaviour as RedisCache).
|
|
14
|
+
*/
|
|
15
|
+
class MemoryCache {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} [options]
|
|
18
|
+
* @param {number} [options.maxEntries=1000] - Entries kept before the least
|
|
19
|
+
* recently used is evicted
|
|
20
|
+
* @param {number} [options.ttlSeconds] - Default time to live; none means
|
|
21
|
+
* entries never expire on their own
|
|
22
|
+
*/
|
|
23
|
+
constructor({ maxEntries = 1000, ttlSeconds } = {}) {
|
|
24
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
25
|
+
throw new Error('maxEntries must be a positive integer');
|
|
26
|
+
}
|
|
27
|
+
this.maxEntries = maxEntries;
|
|
28
|
+
this.ttlSeconds = ttlSeconds;
|
|
29
|
+
this._entries = new Map(); // insertion order = recency order
|
|
30
|
+
this._hits = 0;
|
|
31
|
+
this._misses = 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} key
|
|
36
|
+
* @returns {Promise<unknown|undefined>}
|
|
37
|
+
*/
|
|
38
|
+
async get(key) {
|
|
39
|
+
const entry = this._entries.get(key);
|
|
40
|
+
if (!entry || (entry.expiresAt !== null && entry.expiresAt <= Date.now())) {
|
|
41
|
+
if (entry) this._entries.delete(key);
|
|
42
|
+
this._misses += 1;
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
// Move to the most recently used position.
|
|
46
|
+
this._entries.delete(key);
|
|
47
|
+
this._entries.set(key, entry);
|
|
48
|
+
this._hits += 1;
|
|
49
|
+
return JSON.parse(entry.json);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} key
|
|
54
|
+
* @param {unknown} value - JSON-serialisable value
|
|
55
|
+
* @param {{ ttlSeconds?: number }} [options]
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
async set(key, value, { ttlSeconds = this.ttlSeconds } = {}) {
|
|
59
|
+
const json = JSON.stringify(value);
|
|
60
|
+
if (json === undefined) return;
|
|
61
|
+
this._entries.delete(key);
|
|
62
|
+
this._entries.set(key, {
|
|
63
|
+
json,
|
|
64
|
+
expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : null,
|
|
65
|
+
});
|
|
66
|
+
while (this._entries.size > this.maxEntries) {
|
|
67
|
+
this._entries.delete(this._entries.keys().next().value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {string} key */
|
|
72
|
+
async delete(key) {
|
|
73
|
+
this._entries.delete(key);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Remove every entry. */
|
|
77
|
+
async clear() {
|
|
78
|
+
this._entries.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @returns {Promise<{ hits: number, misses: number, size: number }>} */
|
|
82
|
+
async stats() {
|
|
83
|
+
return { hits: this._hits, misses: this._misses, size: this._entries.size };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { MemoryCache };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Caching for createRagPipeline({ cache }): three opt-in layers.
|
|
5
|
+
*
|
|
6
|
+
* - embeddings: the query vector, keyed by the embedder's identity and the
|
|
7
|
+
* query text.
|
|
8
|
+
* - retrieval: the retriever's raw results, keyed by the retriever's state,
|
|
9
|
+
* the query and topK. Cached before `filterResults` (guardrail ACLs) and
|
|
10
|
+
* the reranker, so those still run on every request and one user's
|
|
11
|
+
* permissions can never shape another user's results.
|
|
12
|
+
* - generation: the answer, keyed by the llm's identity, the query, the
|
|
13
|
+
* exact documents it was given and the generation options. Skipped when
|
|
14
|
+
* the temperature is above 0.
|
|
15
|
+
*
|
|
16
|
+
* A layer caches only when its component identifies itself through
|
|
17
|
+
* `getCacheKey(role)` or a `cacheKey` string (the built-ins do), so a cache
|
|
18
|
+
* can never return results for a different model, index or configuration.
|
|
19
|
+
* Only successful, completed work is stored: nothing from an aborted or
|
|
20
|
+
* timed-out run.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
const { MemoryCache } = require('./memory-cache');
|
|
25
|
+
|
|
26
|
+
const LAYERS = ['embeddings', 'retrieval', 'generation'];
|
|
27
|
+
const DEFAULT_TTL_SECONDS = {
|
|
28
|
+
embeddings: 30 * 24 * 3600,
|
|
29
|
+
retrieval: 3600,
|
|
30
|
+
generation: 24 * 3600,
|
|
31
|
+
};
|
|
32
|
+
const ROLE = {
|
|
33
|
+
embeddings: 'embedder',
|
|
34
|
+
retrieval: 'retriever',
|
|
35
|
+
generation: 'llm',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Generation options that change the answer and so belong in its key. */
|
|
39
|
+
const GENERATION_OPTIONS = [
|
|
40
|
+
'temperature',
|
|
41
|
+
'maxTokens',
|
|
42
|
+
'maxOutputTokens',
|
|
43
|
+
'topP',
|
|
44
|
+
'model',
|
|
45
|
+
'systemPrompt',
|
|
46
|
+
'seed',
|
|
47
|
+
'stopSequences',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function hash(value) {
|
|
51
|
+
return crypto
|
|
52
|
+
.createHash('sha256')
|
|
53
|
+
.update(JSON.stringify(value))
|
|
54
|
+
.digest('hex');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isBackend(value) {
|
|
58
|
+
return (
|
|
59
|
+
value && typeof value.get === 'function' && typeof value.set === 'function'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A component's identity for a role, or null when it has none. */
|
|
64
|
+
function componentKey(component, role) {
|
|
65
|
+
if (!component) return null;
|
|
66
|
+
if (typeof component.getCacheKey === 'function') {
|
|
67
|
+
const key = component.getCacheKey(role);
|
|
68
|
+
return typeof key === 'string' && key ? key : null;
|
|
69
|
+
}
|
|
70
|
+
return typeof component.cacheKey === 'string' && component.cacheKey
|
|
71
|
+
? component.cacheKey
|
|
72
|
+
: null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const warned = new Set();
|
|
76
|
+
function warnOnce(code, message) {
|
|
77
|
+
if (warned.has(code)) return;
|
|
78
|
+
warned.add(code);
|
|
79
|
+
process.emitWarning(message, { type: 'RagCacheWarning', code });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Normalise the `cache` option of createRagPipeline.
|
|
84
|
+
*
|
|
85
|
+
* - `true`: all three layers in one in-memory cache
|
|
86
|
+
* - a CacheBackend: all three layers in it
|
|
87
|
+
* - `{ embeddings, retrieval, generation, ttlSeconds }`: per layer, each
|
|
88
|
+
* `true` (in-memory), `false`/absent (off) or a CacheBackend;
|
|
89
|
+
* `ttlSeconds: { embeddings, retrieval, generation }` overrides the
|
|
90
|
+
* defaults (30 days, 1 hour, 24 hours)
|
|
91
|
+
*
|
|
92
|
+
* @returns {Object|null} Layer config, or null when caching is off
|
|
93
|
+
*/
|
|
94
|
+
function normalizeCacheOption(option) {
|
|
95
|
+
if (!option) return null;
|
|
96
|
+
const shared =
|
|
97
|
+
option === true ? new MemoryCache() : isBackend(option) ? option : null;
|
|
98
|
+
const layers = {};
|
|
99
|
+
for (const layer of LAYERS) {
|
|
100
|
+
let backend = null;
|
|
101
|
+
if (shared) {
|
|
102
|
+
backend = shared;
|
|
103
|
+
} else if (option[layer] === true) {
|
|
104
|
+
backend = new MemoryCache();
|
|
105
|
+
} else if (isBackend(option[layer])) {
|
|
106
|
+
backend = option[layer];
|
|
107
|
+
}
|
|
108
|
+
if (backend) {
|
|
109
|
+
layers[layer] = {
|
|
110
|
+
backend,
|
|
111
|
+
ttlSeconds:
|
|
112
|
+
(option.ttlSeconds && option.ttlSeconds[layer]) ??
|
|
113
|
+
DEFAULT_TTL_SECONDS[layer],
|
|
114
|
+
hits: 0,
|
|
115
|
+
misses: 0,
|
|
116
|
+
skipped: 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Object.keys(layers).length ? layers : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Per-pipeline cache. Every method returns undefined and does nothing for a
|
|
125
|
+
* layer that is off or whose component has no cache key.
|
|
126
|
+
*/
|
|
127
|
+
class PipelineCache {
|
|
128
|
+
/**
|
|
129
|
+
* @param {Object} layers - normalizeCacheOption() output
|
|
130
|
+
* @param {{ embedder?: Object, retriever?: Object, llm?: Object }} components
|
|
131
|
+
*/
|
|
132
|
+
constructor(layers, components) {
|
|
133
|
+
this.layers = layers;
|
|
134
|
+
this.components = components;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @private */
|
|
138
|
+
_key(layer, parts) {
|
|
139
|
+
const config = this.layers[layer];
|
|
140
|
+
if (!config) return null;
|
|
141
|
+
const component = this.components[ROLE[layer]];
|
|
142
|
+
const id = componentKey(component, ROLE[layer]);
|
|
143
|
+
if (!id) {
|
|
144
|
+
config.skipped += 1;
|
|
145
|
+
warnOnce(
|
|
146
|
+
`RAG_CACHE_NO_KEY_${layer.toUpperCase()}`,
|
|
147
|
+
`The ${layer} cache is off for this ${ROLE[layer]}: it has no getCacheKey(role) method or cacheKey string, so cached results could not be told apart from another configuration's. Add one to enable it.`,
|
|
148
|
+
);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return `${layer}:${hash([id, parts])}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @private */
|
|
155
|
+
async _get(layer, key, status) {
|
|
156
|
+
if (!key) {
|
|
157
|
+
status[layer] = 'skip';
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const config = this.layers[layer];
|
|
161
|
+
let value;
|
|
162
|
+
try {
|
|
163
|
+
value = await config.backend.get(key);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// A cache outage must not fail the run.
|
|
166
|
+
warnOnce('RAG_CACHE_READ_FAILED', `Cache read failed: ${error.message}`);
|
|
167
|
+
value = undefined;
|
|
168
|
+
}
|
|
169
|
+
if (value === undefined) {
|
|
170
|
+
config.misses += 1;
|
|
171
|
+
status[layer] = 'miss';
|
|
172
|
+
} else {
|
|
173
|
+
config.hits += 1;
|
|
174
|
+
status[layer] = 'hit';
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @private */
|
|
180
|
+
async _set(layer, key, value) {
|
|
181
|
+
if (!key || value === undefined) return;
|
|
182
|
+
const config = this.layers[layer];
|
|
183
|
+
try {
|
|
184
|
+
await config.backend.set(key, value, { ttlSeconds: config.ttlSeconds });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
warnOnce(
|
|
187
|
+
'RAG_CACHE_WRITE_FAILED',
|
|
188
|
+
`Cache write failed: ${error.message}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
embeddingKey(query) {
|
|
194
|
+
return this._key('embeddings', { query });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
retrievalKey({ query, queryVector, topK }) {
|
|
198
|
+
// Keyed on the query vector, not just the text, so a different embedder
|
|
199
|
+
// (or a retrained one) never gets another embedder's results; the text
|
|
200
|
+
// stays in the key for keyword and hybrid retrievers.
|
|
201
|
+
return this._key('retrieval', {
|
|
202
|
+
query: query ?? null,
|
|
203
|
+
vector: queryVector ? hash(queryVector) : null,
|
|
204
|
+
topK,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @returns {string|null} The key, or null to skip (temperature above 0,
|
|
210
|
+
* or no llm cache key)
|
|
211
|
+
*/
|
|
212
|
+
generationKey({ query, docs, options }) {
|
|
213
|
+
if (!this.layers.generation) return null;
|
|
214
|
+
const llm = this.components.llm;
|
|
215
|
+
const temperature =
|
|
216
|
+
options.temperature ?? (llm && llm.config && llm.config.temperature);
|
|
217
|
+
if (typeof temperature === 'number' && temperature > 0) {
|
|
218
|
+
this.layers.generation.skipped += 1;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const generationOptions = {};
|
|
222
|
+
for (const name of GENERATION_OPTIONS) {
|
|
223
|
+
if (options[name] !== undefined) generationOptions[name] = options[name];
|
|
224
|
+
}
|
|
225
|
+
const context = (docs || []).map((d) =>
|
|
226
|
+
typeof d === 'string' ? d : [d.id ?? null, d.content ?? d.text ?? ''],
|
|
227
|
+
);
|
|
228
|
+
return this._key('generation', {
|
|
229
|
+
query,
|
|
230
|
+
context: hash(context),
|
|
231
|
+
options: generationOptions,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get(layer, key, status) {
|
|
236
|
+
if (!this.layers[layer]) return Promise.resolve(undefined);
|
|
237
|
+
return this._get(layer, key, status);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
set(layer, key, value) {
|
|
241
|
+
if (!this.layers[layer]) return Promise.resolve();
|
|
242
|
+
return this._set(layer, key, value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Hits, misses and skips per layer since the pipeline was created. */
|
|
246
|
+
stats() {
|
|
247
|
+
const out = {};
|
|
248
|
+
for (const [layer, c] of Object.entries(this.layers)) {
|
|
249
|
+
out[layer] = { hits: c.hits, misses: c.misses, skipped: c.skipped };
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
normalizeCacheOption,
|
|
257
|
+
PipelineCache,
|
|
258
|
+
componentKey,
|
|
259
|
+
DEFAULT_TTL_SECONDS,
|
|
260
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CacheBackend on Redis. Pass a connected client from `redis` (node-redis 4
|
|
5
|
+
* and later) or `ioredis`; neither is a dependency of this package.
|
|
6
|
+
*
|
|
7
|
+
* Values are stored as JSON under `prefix + key`, with a millisecond expiry
|
|
8
|
+
* when a time to live applies.
|
|
9
|
+
*/
|
|
10
|
+
class RedisCache {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {Object} options.client - Connected node-redis or ioredis client
|
|
14
|
+
* @param {string} [options.prefix='rag:'] - Key prefix
|
|
15
|
+
* @param {number} [options.ttlSeconds] - Default time to live
|
|
16
|
+
*/
|
|
17
|
+
constructor({ client, prefix = 'rag:', ttlSeconds } = {}) {
|
|
18
|
+
if (
|
|
19
|
+
!client ||
|
|
20
|
+
typeof client.get !== 'function' ||
|
|
21
|
+
typeof client.set !== 'function'
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'RedisCache needs a connected client from "redis" or "ioredis"',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.prefix = prefix;
|
|
29
|
+
this.ttlSeconds = ttlSeconds;
|
|
30
|
+
// node-redis 4+ exposes isOpen and takes set(key, value, { PX }); ioredis
|
|
31
|
+
// takes set(key, value, 'PX', ms).
|
|
32
|
+
this._nodeRedis = typeof client.isOpen === 'boolean';
|
|
33
|
+
this._hits = 0;
|
|
34
|
+
this._misses = 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} key
|
|
39
|
+
* @returns {Promise<unknown|undefined>}
|
|
40
|
+
*/
|
|
41
|
+
async get(key) {
|
|
42
|
+
const raw = await this.client.get(this.prefix + key);
|
|
43
|
+
if (raw === null || raw === undefined) {
|
|
44
|
+
this._misses += 1;
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
this._hits += 1;
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} key
|
|
53
|
+
* @param {unknown} value - JSON-serialisable value
|
|
54
|
+
* @param {{ ttlSeconds?: number }} [options]
|
|
55
|
+
*/
|
|
56
|
+
async set(key, value, { ttlSeconds = this.ttlSeconds } = {}) {
|
|
57
|
+
const json = JSON.stringify(value);
|
|
58
|
+
if (json === undefined) return;
|
|
59
|
+
const k = this.prefix + key;
|
|
60
|
+
const ms = ttlSeconds > 0 ? Math.round(ttlSeconds * 1000) : 0;
|
|
61
|
+
if (!ms) {
|
|
62
|
+
await this.client.set(k, json);
|
|
63
|
+
} else if (this._nodeRedis) {
|
|
64
|
+
await this.client.set(k, json, { PX: ms });
|
|
65
|
+
} else {
|
|
66
|
+
await this.client.set(k, json, 'PX', ms);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {string} key */
|
|
71
|
+
async delete(key) {
|
|
72
|
+
const del = this.client.del || this.client.unlink;
|
|
73
|
+
if (typeof del === 'function')
|
|
74
|
+
await del.call(this.client, this.prefix + key);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Hits and misses seen by this instance (size is not tracked). */
|
|
78
|
+
async stats() {
|
|
79
|
+
return { hits: this._hits, misses: this._misses };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { RedisCache };
|
package/src/cli/commands/ai.js
CHANGED
|
@@ -7,13 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
const { Command, Option } = require("commander");
|
|
9
9
|
const { parsePositiveInteger } = require("../shared/cli-utils.js");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
handleFeedback,
|
|
14
|
-
handleSearch,
|
|
15
|
-
handleMultimodalProcess,
|
|
16
|
-
} = require("../handlers/ai-handler.js");
|
|
10
|
+
// The handler (document loaders: jsdom, marked, csv-parse) loads when an
|
|
11
|
+
// ai command runs, not when the CLI starts.
|
|
12
|
+
const aiHandler = () => require("../handlers/ai-handler.js"); // eslint-disable-line global-require
|
|
17
13
|
|
|
18
14
|
/**
|
|
19
15
|
* @param {() => object} getGlobalOptions - Returns program.opts()
|
|
@@ -37,7 +33,7 @@ function createAiCommand(getGlobalOptions) {
|
|
|
37
33
|
"Train on documents from this file or directory instead",
|
|
38
34
|
)
|
|
39
35
|
.action(async (inputPath) => {
|
|
40
|
-
await handleTrainEmbedding(getGlobalOptions(), inputPath);
|
|
36
|
+
await aiHandler().handleTrainEmbedding(getGlobalOptions(), inputPath);
|
|
41
37
|
});
|
|
42
38
|
|
|
43
39
|
ai.command("benchmark")
|
|
@@ -52,7 +48,7 @@ function createAiCommand(getGlobalOptions) {
|
|
|
52
48
|
.option("--top-k <n>", "Passages to retrieve", parsePositiveInteger, 3)
|
|
53
49
|
.option("--json", "Print the measurements as JSON")
|
|
54
50
|
.action(async (options) => {
|
|
55
|
-
await handleBenchmark(getGlobalOptions(), options);
|
|
51
|
+
await aiHandler().handleBenchmark(getGlobalOptions(), options);
|
|
56
52
|
});
|
|
57
53
|
|
|
58
54
|
ai.command("feedback")
|
|
@@ -67,7 +63,12 @@ function createAiCommand(getGlobalOptions) {
|
|
|
67
63
|
.makeOptionMandatory(),
|
|
68
64
|
)
|
|
69
65
|
.action(async (question, docId, options) => {
|
|
70
|
-
await handleFeedback(
|
|
66
|
+
await aiHandler().handleFeedback(
|
|
67
|
+
getGlobalOptions(),
|
|
68
|
+
question,
|
|
69
|
+
docId,
|
|
70
|
+
options,
|
|
71
|
+
);
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
ai.command("search")
|
|
@@ -78,7 +79,7 @@ function createAiCommand(getGlobalOptions) {
|
|
|
78
79
|
.option("--top-k <n>", "Passages to return", parsePositiveInteger, 3)
|
|
79
80
|
.option("--json", "Print the results as JSON")
|
|
80
81
|
.action(async (question, options) => {
|
|
81
|
-
await handleSearch(getGlobalOptions(), question, options);
|
|
82
|
+
await aiHandler().handleSearch(getGlobalOptions(), question, options);
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
const multimodal = ai
|
|
@@ -92,7 +93,11 @@ function createAiCommand(getGlobalOptions) {
|
|
|
92
93
|
.argument("<path>", "File or directory")
|
|
93
94
|
.option("--json", "Print the report as JSON")
|
|
94
95
|
.action(async (inputPath, options) => {
|
|
95
|
-
await handleMultimodalProcess(
|
|
96
|
+
await aiHandler().handleMultimodalProcess(
|
|
97
|
+
getGlobalOptions(),
|
|
98
|
+
inputPath,
|
|
99
|
+
options,
|
|
100
|
+
);
|
|
96
101
|
});
|
|
97
102
|
|
|
98
103
|
return ai;
|
|
@@ -7,9 +7,14 @@ const { Command } = require("commander");
|
|
|
7
7
|
const { PluginHub } = require("../../ecosystem/plugin-hub");
|
|
8
8
|
const { PluginCertification } = require("../../ecosystem/plugin-certification");
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// Command modules (and inquirer, ora, cli-table3) load when a hub command
|
|
11
|
+
// runs, not when the CLI starts.
|
|
12
|
+
/* eslint-disable global-require */
|
|
13
|
+
const hubCommands = () => require("./plugin-hub/hub-commands");
|
|
14
|
+
const certificationCommands = () =>
|
|
15
|
+
require("./plugin-hub/certification-commands");
|
|
16
|
+
const reviewCommands = () => require("./plugin-hub/review-commands");
|
|
17
|
+
/* eslint-enable global-require */
|
|
13
18
|
const { setupEventListeners } = require("./plugin-hub/event-handlers");
|
|
14
19
|
const {
|
|
15
20
|
installFromSource,
|
|
@@ -102,7 +107,7 @@ class PluginHubCLI {
|
|
|
102
107
|
"relevance",
|
|
103
108
|
)
|
|
104
109
|
.action(async (query, _options) => {
|
|
105
|
-
await hubCommands.searchPlugins(this.hub, query, _options);
|
|
110
|
+
await hubCommands().searchPlugins(this.hub, query, _options);
|
|
106
111
|
});
|
|
107
112
|
|
|
108
113
|
program
|
|
@@ -128,7 +133,7 @@ class PluginHubCLI {
|
|
|
128
133
|
.command("info <plugin>")
|
|
129
134
|
.description("Get detailed information about a plugin")
|
|
130
135
|
.action(async (plugin) => {
|
|
131
|
-
await hubCommands.getPluginInfo(this.hub, plugin);
|
|
136
|
+
await hubCommands().getPluginInfo(this.hub, plugin);
|
|
132
137
|
});
|
|
133
138
|
|
|
134
139
|
// List installed command
|
|
@@ -148,7 +153,7 @@ class PluginHubCLI {
|
|
|
148
153
|
.option("--dry-run", "Validate without publishing")
|
|
149
154
|
.option("--tag <tag>", "Release tag")
|
|
150
155
|
.action(async (pluginPath, _options) => {
|
|
151
|
-
await hubCommands.publishPlugin(
|
|
156
|
+
await hubCommands().publishPlugin(
|
|
152
157
|
this.hub,
|
|
153
158
|
pluginPath || process.cwd(),
|
|
154
159
|
_options,
|
|
@@ -161,7 +166,7 @@ class PluginHubCLI {
|
|
|
161
166
|
.description("Rate a plugin (1-5 stars)")
|
|
162
167
|
.option("-r, --review <review>", "Written review")
|
|
163
168
|
.action(async (plugin, rating, _options) => {
|
|
164
|
-
await reviewCommands.ratePlugin(
|
|
169
|
+
await reviewCommands().ratePlugin(
|
|
165
170
|
this.hub,
|
|
166
171
|
plugin,
|
|
167
172
|
parseInt(rating),
|
|
@@ -176,7 +181,7 @@ class PluginHubCLI {
|
|
|
176
181
|
.option("-l, --limit <limit>", "Number of reviews", parseInt, 10)
|
|
177
182
|
.option("--sort <sort>", "Sort by: helpful, recent, rating", "helpful")
|
|
178
183
|
.action(async (plugin, _options) => {
|
|
179
|
-
await reviewCommands.getPluginReviews(this.hub, plugin, _options);
|
|
184
|
+
await reviewCommands().getPluginReviews(this.hub, plugin, _options);
|
|
180
185
|
});
|
|
181
186
|
|
|
182
187
|
// Trending command
|
|
@@ -187,7 +192,7 @@ class PluginHubCLI {
|
|
|
187
192
|
.option("-c, --category <category>", "Filter by category")
|
|
188
193
|
.option("-l, --limit <limit>", "Number of results", parseInt, 20)
|
|
189
194
|
.action(async (_options) => {
|
|
190
|
-
await hubCommands.getTrendingPlugins(this.hub, _options);
|
|
195
|
+
await hubCommands().getTrendingPlugins(this.hub, _options);
|
|
191
196
|
});
|
|
192
197
|
|
|
193
198
|
// Certification commands
|
|
@@ -210,7 +215,7 @@ class PluginHubCLI {
|
|
|
210
215
|
.command("verify <plugin> <certificationId>")
|
|
211
216
|
.description("Verify plugin certification")
|
|
212
217
|
.action(async (plugin, certificationId) => {
|
|
213
|
-
await certificationCommands.verifyCertification(
|
|
218
|
+
await certificationCommands().verifyCertification(
|
|
214
219
|
this.certification,
|
|
215
220
|
plugin,
|
|
216
221
|
certificationId,
|
|
@@ -222,7 +227,7 @@ class PluginHubCLI {
|
|
|
222
227
|
.description("Show certification requirements")
|
|
223
228
|
.action(async (level) => {
|
|
224
229
|
// Requirements are a static list; no registry is contacted.
|
|
225
|
-
await certificationCommands.showCertificationRequirements(
|
|
230
|
+
await certificationCommands().showCertificationRequirements(
|
|
226
231
|
new PluginCertification(),
|
|
227
232
|
level,
|
|
228
233
|
);
|
|
@@ -237,7 +242,7 @@ class PluginHubCLI {
|
|
|
237
242
|
.command("status [publisherId]")
|
|
238
243
|
.description("Check publisher verification status")
|
|
239
244
|
.action(async (publisherId) => {
|
|
240
|
-
await certificationCommands.getPublisherStatus(
|
|
245
|
+
await certificationCommands().getPublisherStatus(
|
|
241
246
|
this.certification,
|
|
242
247
|
publisherId,
|
|
243
248
|
);
|
|
@@ -247,7 +252,7 @@ class PluginHubCLI {
|
|
|
247
252
|
.command("apply")
|
|
248
253
|
.description("Apply for publisher verification")
|
|
249
254
|
.action(async () => {
|
|
250
|
-
await certificationCommands.applyForPublisherVerification(
|
|
255
|
+
await certificationCommands().applyForPublisherVerification(
|
|
251
256
|
this.certification,
|
|
252
257
|
);
|
|
253
258
|
});
|