s3db.js 12.4.0 → 13.0.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/dist/s3db.cjs.js +1923 -57
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1923 -58
- package/dist/s3db.es.js.map +1 -1
- package/package.json +5 -1
- package/src/clients/memory-client.class.js +41 -24
- package/src/database.class.js +52 -17
- package/src/plugins/api/index.js +12 -9
- package/src/plugins/api/routes/resource-routes.js +78 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/ml/base-model.class.js +459 -0
- package/src/plugins/ml/classification-model.class.js +338 -0
- package/src/plugins/ml/neural-network-model.class.js +312 -0
- package/src/plugins/ml/regression-model.class.js +159 -0
- package/src/plugins/ml/timeseries-model.class.js +346 -0
- package/src/plugins/ml.errors.js +130 -0
- package/src/plugins/ml.plugin.js +655 -0
- package/src/resource.class.js +106 -34
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine Learning Plugin
|
|
3
|
+
*
|
|
4
|
+
* Train and use ML models directly on s3db.js resources
|
|
5
|
+
* Supports regression, classification, time series, and custom neural networks
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Plugin from './plugin.class.js';
|
|
9
|
+
import { requirePluginDependency } from './concerns/plugin-dependencies.js';
|
|
10
|
+
import tryFn from '../concerns/try-fn.js';
|
|
11
|
+
|
|
12
|
+
import { RegressionModel } from './ml/regression-model.class.js';
|
|
13
|
+
import { ClassificationModel } from './ml/classification-model.class.js';
|
|
14
|
+
import { TimeSeriesModel } from './ml/timeseries-model.class.js';
|
|
15
|
+
import { NeuralNetworkModel } from './ml/neural-network-model.class.js';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
MLError,
|
|
19
|
+
ModelConfigError,
|
|
20
|
+
ModelNotFoundError,
|
|
21
|
+
TrainingError,
|
|
22
|
+
TensorFlowDependencyError
|
|
23
|
+
} from './ml.errors.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ML Plugin Configuration
|
|
27
|
+
*
|
|
28
|
+
* @typedef {Object} MLPluginOptions
|
|
29
|
+
* @property {Object} models - Model configurations
|
|
30
|
+
* @property {boolean} [verbose=false] - Enable verbose logging
|
|
31
|
+
* @property {number} [minTrainingSamples=10] - Minimum samples required for training
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* new MLPlugin({
|
|
35
|
+
* models: {
|
|
36
|
+
* productPrices: {
|
|
37
|
+
* type: 'regression',
|
|
38
|
+
* resource: 'products',
|
|
39
|
+
* features: ['cost', 'margin', 'demand'],
|
|
40
|
+
* target: 'price',
|
|
41
|
+
* autoTrain: true,
|
|
42
|
+
* trainInterval: 3600000, // 1 hour
|
|
43
|
+
* trainAfterInserts: 100,
|
|
44
|
+
* modelConfig: {
|
|
45
|
+
* epochs: 50,
|
|
46
|
+
* batchSize: 32,
|
|
47
|
+
* learningRate: 0.01
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* },
|
|
51
|
+
* verbose: true
|
|
52
|
+
* })
|
|
53
|
+
*/
|
|
54
|
+
export class MLPlugin extends Plugin {
|
|
55
|
+
constructor(options = {}) {
|
|
56
|
+
super(options);
|
|
57
|
+
|
|
58
|
+
this.config = {
|
|
59
|
+
models: options.models || {},
|
|
60
|
+
verbose: options.verbose || false,
|
|
61
|
+
minTrainingSamples: options.minTrainingSamples || 10
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Validate TensorFlow.js dependency
|
|
65
|
+
requirePluginDependency('@tensorflow/tfjs-node', 'MLPlugin', {
|
|
66
|
+
installCommand: 'pnpm add @tensorflow/tfjs-node',
|
|
67
|
+
reason: 'Required for machine learning model training and inference'
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Model instances
|
|
71
|
+
this.models = {};
|
|
72
|
+
|
|
73
|
+
// Training state
|
|
74
|
+
this.training = new Map(); // Track ongoing training
|
|
75
|
+
this.insertCounters = new Map(); // Track inserts per resource
|
|
76
|
+
|
|
77
|
+
// Interval handles for auto-training
|
|
78
|
+
this.intervals = [];
|
|
79
|
+
|
|
80
|
+
// Stats
|
|
81
|
+
this.stats = {
|
|
82
|
+
totalTrainings: 0,
|
|
83
|
+
totalPredictions: 0,
|
|
84
|
+
totalErrors: 0,
|
|
85
|
+
startedAt: null
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Install the plugin
|
|
91
|
+
*/
|
|
92
|
+
async onInstall() {
|
|
93
|
+
if (this.config.verbose) {
|
|
94
|
+
console.log('[MLPlugin] Installing ML Plugin...');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate model configurations
|
|
98
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
99
|
+
this._validateModelConfig(modelName, modelConfig);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Initialize models
|
|
103
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
104
|
+
await this._initializeModel(modelName, modelConfig);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Setup auto-training hooks
|
|
108
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
109
|
+
if (modelConfig.autoTrain) {
|
|
110
|
+
this._setupAutoTraining(modelName, modelConfig);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.stats.startedAt = new Date().toISOString();
|
|
115
|
+
|
|
116
|
+
if (this.config.verbose) {
|
|
117
|
+
console.log(`[MLPlugin] Installed with ${Object.keys(this.models).length} models`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.emit('installed', {
|
|
121
|
+
plugin: 'MLPlugin',
|
|
122
|
+
models: Object.keys(this.models)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start the plugin
|
|
128
|
+
*/
|
|
129
|
+
async onStart() {
|
|
130
|
+
// Try to load previously trained models
|
|
131
|
+
for (const modelName of Object.keys(this.models)) {
|
|
132
|
+
await this._loadModel(modelName);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.config.verbose) {
|
|
136
|
+
console.log('[MLPlugin] Started');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stop the plugin
|
|
142
|
+
*/
|
|
143
|
+
async onStop() {
|
|
144
|
+
// Stop all intervals
|
|
145
|
+
for (const handle of this.intervals) {
|
|
146
|
+
clearInterval(handle);
|
|
147
|
+
}
|
|
148
|
+
this.intervals = [];
|
|
149
|
+
|
|
150
|
+
// Dispose all models
|
|
151
|
+
for (const [modelName, model] of Object.entries(this.models)) {
|
|
152
|
+
if (model && model.dispose) {
|
|
153
|
+
model.dispose();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this.config.verbose) {
|
|
158
|
+
console.log('[MLPlugin] Stopped');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Uninstall the plugin
|
|
164
|
+
*/
|
|
165
|
+
async onUninstall(options = {}) {
|
|
166
|
+
await this.onStop();
|
|
167
|
+
|
|
168
|
+
if (options.purgeData) {
|
|
169
|
+
// Delete all saved models from plugin storage
|
|
170
|
+
for (const modelName of Object.keys(this.models)) {
|
|
171
|
+
await this._deleteModel(modelName);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.config.verbose) {
|
|
175
|
+
console.log('[MLPlugin] Purged all model data');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate model configuration
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_validateModelConfig(modelName, config) {
|
|
185
|
+
const validTypes = ['regression', 'classification', 'timeseries', 'neural-network'];
|
|
186
|
+
|
|
187
|
+
if (!config.type || !validTypes.includes(config.type)) {
|
|
188
|
+
throw new ModelConfigError(
|
|
189
|
+
`Model "${modelName}" must have a valid type: ${validTypes.join(', ')}`,
|
|
190
|
+
{ modelName, type: config.type, validTypes }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!config.resource) {
|
|
195
|
+
throw new ModelConfigError(
|
|
196
|
+
`Model "${modelName}" must specify a resource`,
|
|
197
|
+
{ modelName }
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!config.features || !Array.isArray(config.features) || config.features.length === 0) {
|
|
202
|
+
throw new ModelConfigError(
|
|
203
|
+
`Model "${modelName}" must specify at least one feature`,
|
|
204
|
+
{ modelName, features: config.features }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!config.target) {
|
|
209
|
+
throw new ModelConfigError(
|
|
210
|
+
`Model "${modelName}" must specify a target field`,
|
|
211
|
+
{ modelName }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Initialize a model instance
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
async _initializeModel(modelName, config) {
|
|
221
|
+
const modelOptions = {
|
|
222
|
+
name: modelName,
|
|
223
|
+
resource: config.resource,
|
|
224
|
+
features: config.features,
|
|
225
|
+
target: config.target,
|
|
226
|
+
modelConfig: config.modelConfig || {},
|
|
227
|
+
verbose: this.config.verbose
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
switch (config.type) {
|
|
232
|
+
case 'regression':
|
|
233
|
+
this.models[modelName] = new RegressionModel(modelOptions);
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'classification':
|
|
237
|
+
this.models[modelName] = new ClassificationModel(modelOptions);
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'timeseries':
|
|
241
|
+
this.models[modelName] = new TimeSeriesModel(modelOptions);
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
case 'neural-network':
|
|
245
|
+
this.models[modelName] = new NeuralNetworkModel(modelOptions);
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
default:
|
|
249
|
+
throw new ModelConfigError(
|
|
250
|
+
`Unknown model type: ${config.type}`,
|
|
251
|
+
{ modelName, type: config.type }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (this.config.verbose) {
|
|
256
|
+
console.log(`[MLPlugin] Initialized model "${modelName}" (${config.type})`);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(`[MLPlugin] Failed to initialize model "${modelName}":`, error.message);
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Setup auto-training for a model
|
|
266
|
+
* @private
|
|
267
|
+
*/
|
|
268
|
+
_setupAutoTraining(modelName, config) {
|
|
269
|
+
const resource = this.database.resources[config.resource];
|
|
270
|
+
|
|
271
|
+
if (!resource) {
|
|
272
|
+
console.warn(`[MLPlugin] Resource "${config.resource}" not found for model "${modelName}"`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Initialize insert counter
|
|
277
|
+
this.insertCounters.set(modelName, 0);
|
|
278
|
+
|
|
279
|
+
// Hook: Track inserts
|
|
280
|
+
if (config.trainAfterInserts && config.trainAfterInserts > 0) {
|
|
281
|
+
this.addMiddleware(resource, 'insert', async (next, data, options) => {
|
|
282
|
+
const result = await next(data, options);
|
|
283
|
+
|
|
284
|
+
// Increment counter
|
|
285
|
+
const currentCount = this.insertCounters.get(modelName) || 0;
|
|
286
|
+
this.insertCounters.set(modelName, currentCount + 1);
|
|
287
|
+
|
|
288
|
+
// Check if we should train
|
|
289
|
+
if (this.insertCounters.get(modelName) >= config.trainAfterInserts) {
|
|
290
|
+
if (this.config.verbose) {
|
|
291
|
+
console.log(`[MLPlugin] Auto-training "${modelName}" after ${config.trainAfterInserts} inserts`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Reset counter
|
|
295
|
+
this.insertCounters.set(modelName, 0);
|
|
296
|
+
|
|
297
|
+
// Train asynchronously (don't block insert)
|
|
298
|
+
this.train(modelName).catch(err => {
|
|
299
|
+
console.error(`[MLPlugin] Auto-training failed for "${modelName}":`, err.message);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return result;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Interval-based training
|
|
308
|
+
if (config.trainInterval && config.trainInterval > 0) {
|
|
309
|
+
const handle = setInterval(async () => {
|
|
310
|
+
if (this.config.verbose) {
|
|
311
|
+
console.log(`[MLPlugin] Auto-training "${modelName}" (interval: ${config.trainInterval}ms)`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await this.train(modelName);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error(`[MLPlugin] Auto-training failed for "${modelName}":`, error.message);
|
|
318
|
+
}
|
|
319
|
+
}, config.trainInterval);
|
|
320
|
+
|
|
321
|
+
this.intervals.push(handle);
|
|
322
|
+
|
|
323
|
+
if (this.config.verbose) {
|
|
324
|
+
console.log(`[MLPlugin] Setup interval training for "${modelName}" (every ${config.trainInterval}ms)`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Train a model
|
|
331
|
+
* @param {string} modelName - Model name
|
|
332
|
+
* @param {Object} options - Training options
|
|
333
|
+
* @returns {Object} Training results
|
|
334
|
+
*/
|
|
335
|
+
async train(modelName, options = {}) {
|
|
336
|
+
const model = this.models[modelName];
|
|
337
|
+
if (!model) {
|
|
338
|
+
throw new ModelNotFoundError(
|
|
339
|
+
`Model "${modelName}" not found`,
|
|
340
|
+
{ modelName, availableModels: Object.keys(this.models) }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check if already training
|
|
345
|
+
if (this.training.get(modelName)) {
|
|
346
|
+
if (this.config.verbose) {
|
|
347
|
+
console.log(`[MLPlugin] Model "${modelName}" is already training, skipping...`);
|
|
348
|
+
}
|
|
349
|
+
return { skipped: true, reason: 'already_training' };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Mark as training
|
|
353
|
+
this.training.set(modelName, true);
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// Get model config
|
|
357
|
+
const modelConfig = this.config.models[modelName];
|
|
358
|
+
|
|
359
|
+
// Get resource
|
|
360
|
+
const resource = this.database.resources[modelConfig.resource];
|
|
361
|
+
if (!resource) {
|
|
362
|
+
throw new ModelNotFoundError(
|
|
363
|
+
`Resource "${modelConfig.resource}" not found`,
|
|
364
|
+
{ modelName, resource: modelConfig.resource }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Fetch training data
|
|
369
|
+
if (this.config.verbose) {
|
|
370
|
+
console.log(`[MLPlugin] Fetching training data for "${modelName}"...`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const [ok, err, data] = await tryFn(() => resource.list());
|
|
374
|
+
|
|
375
|
+
if (!ok) {
|
|
376
|
+
throw new TrainingError(
|
|
377
|
+
`Failed to fetch training data: ${err.message}`,
|
|
378
|
+
{ modelName, resource: modelConfig.resource, originalError: err.message }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!data || data.length < this.config.minTrainingSamples) {
|
|
383
|
+
throw new TrainingError(
|
|
384
|
+
`Insufficient training data: ${data?.length || 0} samples (minimum: ${this.config.minTrainingSamples})`,
|
|
385
|
+
{ modelName, samples: data?.length || 0, minimum: this.config.minTrainingSamples }
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (this.config.verbose) {
|
|
390
|
+
console.log(`[MLPlugin] Training "${modelName}" with ${data.length} samples...`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Train model
|
|
394
|
+
const result = await model.train(data);
|
|
395
|
+
|
|
396
|
+
// Save model to plugin storage
|
|
397
|
+
await this._saveModel(modelName);
|
|
398
|
+
|
|
399
|
+
this.stats.totalTrainings++;
|
|
400
|
+
|
|
401
|
+
if (this.config.verbose) {
|
|
402
|
+
console.log(`[MLPlugin] Training completed for "${modelName}":`, result);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.emit('modelTrained', {
|
|
406
|
+
modelName,
|
|
407
|
+
type: modelConfig.type,
|
|
408
|
+
result
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return result;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
this.stats.totalErrors++;
|
|
414
|
+
|
|
415
|
+
if (error instanceof MLError) {
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
throw new TrainingError(
|
|
420
|
+
`Training failed for "${modelName}": ${error.message}`,
|
|
421
|
+
{ modelName, originalError: error.message }
|
|
422
|
+
);
|
|
423
|
+
} finally {
|
|
424
|
+
this.training.set(modelName, false);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Make a prediction
|
|
430
|
+
* @param {string} modelName - Model name
|
|
431
|
+
* @param {Object|Array} input - Input data (object for single prediction, array for time series)
|
|
432
|
+
* @returns {Object} Prediction result
|
|
433
|
+
*/
|
|
434
|
+
async predict(modelName, input) {
|
|
435
|
+
const model = this.models[modelName];
|
|
436
|
+
if (!model) {
|
|
437
|
+
throw new ModelNotFoundError(
|
|
438
|
+
`Model "${modelName}" not found`,
|
|
439
|
+
{ modelName, availableModels: Object.keys(this.models) }
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const result = await model.predict(input);
|
|
445
|
+
this.stats.totalPredictions++;
|
|
446
|
+
|
|
447
|
+
this.emit('prediction', {
|
|
448
|
+
modelName,
|
|
449
|
+
input,
|
|
450
|
+
result
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return result;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
this.stats.totalErrors++;
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Make predictions for multiple inputs
|
|
462
|
+
* @param {string} modelName - Model name
|
|
463
|
+
* @param {Array} inputs - Array of input objects
|
|
464
|
+
* @returns {Array} Array of prediction results
|
|
465
|
+
*/
|
|
466
|
+
async predictBatch(modelName, inputs) {
|
|
467
|
+
const model = this.models[modelName];
|
|
468
|
+
if (!model) {
|
|
469
|
+
throw new ModelNotFoundError(
|
|
470
|
+
`Model "${modelName}" not found`,
|
|
471
|
+
{ modelName, availableModels: Object.keys(this.models) }
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return await model.predictBatch(inputs);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Retrain a model (reset and train from scratch)
|
|
480
|
+
* @param {string} modelName - Model name
|
|
481
|
+
* @param {Object} options - Options
|
|
482
|
+
* @returns {Object} Training results
|
|
483
|
+
*/
|
|
484
|
+
async retrain(modelName, options = {}) {
|
|
485
|
+
const model = this.models[modelName];
|
|
486
|
+
if (!model) {
|
|
487
|
+
throw new ModelNotFoundError(
|
|
488
|
+
`Model "${modelName}" not found`,
|
|
489
|
+
{ modelName, availableModels: Object.keys(this.models) }
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Dispose current model
|
|
494
|
+
if (model.dispose) {
|
|
495
|
+
model.dispose();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Re-initialize
|
|
499
|
+
const modelConfig = this.config.models[modelName];
|
|
500
|
+
await this._initializeModel(modelName, modelConfig);
|
|
501
|
+
|
|
502
|
+
// Train
|
|
503
|
+
return await this.train(modelName, options);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Get model statistics
|
|
508
|
+
* @param {string} modelName - Model name
|
|
509
|
+
* @returns {Object} Model stats
|
|
510
|
+
*/
|
|
511
|
+
getModelStats(modelName) {
|
|
512
|
+
const model = this.models[modelName];
|
|
513
|
+
if (!model) {
|
|
514
|
+
throw new ModelNotFoundError(
|
|
515
|
+
`Model "${modelName}" not found`,
|
|
516
|
+
{ modelName, availableModels: Object.keys(this.models) }
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return model.getStats();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get plugin statistics
|
|
525
|
+
* @returns {Object} Plugin stats
|
|
526
|
+
*/
|
|
527
|
+
getStats() {
|
|
528
|
+
return {
|
|
529
|
+
...this.stats,
|
|
530
|
+
models: Object.keys(this.models).length,
|
|
531
|
+
trainedModels: Object.values(this.models).filter(m => m.isTrained).length
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Export a model
|
|
537
|
+
* @param {string} modelName - Model name
|
|
538
|
+
* @returns {Object} Serialized model
|
|
539
|
+
*/
|
|
540
|
+
async exportModel(modelName) {
|
|
541
|
+
const model = this.models[modelName];
|
|
542
|
+
if (!model) {
|
|
543
|
+
throw new ModelNotFoundError(
|
|
544
|
+
`Model "${modelName}" not found`,
|
|
545
|
+
{ modelName, availableModels: Object.keys(this.models) }
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return await model.export();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Import a model
|
|
554
|
+
* @param {string} modelName - Model name
|
|
555
|
+
* @param {Object} data - Serialized model data
|
|
556
|
+
*/
|
|
557
|
+
async importModel(modelName, data) {
|
|
558
|
+
const model = this.models[modelName];
|
|
559
|
+
if (!model) {
|
|
560
|
+
throw new ModelNotFoundError(
|
|
561
|
+
`Model "${modelName}" not found`,
|
|
562
|
+
{ modelName, availableModels: Object.keys(this.models) }
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await model.import(data);
|
|
567
|
+
|
|
568
|
+
// Save to plugin storage
|
|
569
|
+
await this._saveModel(modelName);
|
|
570
|
+
|
|
571
|
+
if (this.config.verbose) {
|
|
572
|
+
console.log(`[MLPlugin] Imported model "${modelName}"`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Save model to plugin storage
|
|
578
|
+
* @private
|
|
579
|
+
*/
|
|
580
|
+
async _saveModel(modelName) {
|
|
581
|
+
try {
|
|
582
|
+
const storage = this.getStorage();
|
|
583
|
+
const exportedModel = await this.models[modelName].export();
|
|
584
|
+
|
|
585
|
+
if (!exportedModel) {
|
|
586
|
+
if (this.config.verbose) {
|
|
587
|
+
console.log(`[MLPlugin] Model "${modelName}" not trained, skipping save`);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Use patch() for faster metadata-only updates (enforce-limits behavior)
|
|
593
|
+
await storage.patch(`model_${modelName}`, {
|
|
594
|
+
modelName,
|
|
595
|
+
data: JSON.stringify(exportedModel),
|
|
596
|
+
savedAt: new Date().toISOString()
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (this.config.verbose) {
|
|
600
|
+
console.log(`[MLPlugin] Saved model "${modelName}" to plugin storage`);
|
|
601
|
+
}
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error(`[MLPlugin] Failed to save model "${modelName}":`, error.message);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Load model from plugin storage
|
|
609
|
+
* @private
|
|
610
|
+
*/
|
|
611
|
+
async _loadModel(modelName) {
|
|
612
|
+
try {
|
|
613
|
+
const storage = this.getStorage();
|
|
614
|
+
const [ok, err, record] = await tryFn(() => storage.get(`model_${modelName}`));
|
|
615
|
+
|
|
616
|
+
if (!ok || !record) {
|
|
617
|
+
if (this.config.verbose) {
|
|
618
|
+
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const modelData = JSON.parse(record.data);
|
|
624
|
+
await this.models[modelName].import(modelData);
|
|
625
|
+
|
|
626
|
+
if (this.config.verbose) {
|
|
627
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" from plugin storage`);
|
|
628
|
+
}
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error(`[MLPlugin] Failed to load model "${modelName}":`, error.message);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Delete model from plugin storage
|
|
636
|
+
* @private
|
|
637
|
+
*/
|
|
638
|
+
async _deleteModel(modelName) {
|
|
639
|
+
try {
|
|
640
|
+
const storage = this.getStorage();
|
|
641
|
+
await storage.delete(`model_${modelName}`);
|
|
642
|
+
|
|
643
|
+
if (this.config.verbose) {
|
|
644
|
+
console.log(`[MLPlugin] Deleted model "${modelName}" from plugin storage`);
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
// Ignore errors (model might not exist)
|
|
648
|
+
if (this.config.verbose) {
|
|
649
|
+
console.log(`[MLPlugin] Could not delete model "${modelName}": ${error.message}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export default MLPlugin;
|