s3db.js 13.0.0 → 13.1.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 +2681 -43
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +2681 -43
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/api/index.js +1 -7
- package/src/plugins/api/routes/resource-routes.js +3 -3
- package/src/plugins/api/server.js +29 -9
- package/src/plugins/audit.plugin.js +2 -4
- package/src/plugins/backup.plugin.js +2 -4
- package/src/plugins/cache.plugin.js +1 -3
- package/src/plugins/costs.plugin.js +0 -2
- package/src/plugins/eventual-consistency/index.js +1 -3
- package/src/plugins/fulltext.plugin.js +2 -4
- package/src/plugins/geo.plugin.js +1 -3
- package/src/plugins/importer/index.js +0 -2
- package/src/plugins/index.js +0 -1
- package/src/plugins/metrics.plugin.js +2 -4
- package/src/plugins/ml.plugin.js +794 -32
- package/src/plugins/plugin.class.js +1 -3
- package/src/plugins/queue-consumer.plugin.js +1 -3
- package/src/plugins/relation.plugin.js +1 -3
- package/src/plugins/replicator.plugin.js +2 -4
- package/src/plugins/s3-queue.plugin.js +1 -3
- package/src/plugins/scheduler.plugin.js +2 -4
- package/src/plugins/state-machine.plugin.js +2 -4
- package/src/plugins/tfstate/index.js +0 -2
- package/src/plugins/ttl.plugin.js +36 -21
- package/src/plugins/vector.plugin.js +0 -2
package/src/plugins/ml.plugin.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Supports regression, classification, time series, and custom neural networks
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import Plugin from './plugin.class.js';
|
|
8
|
+
import { Plugin } from './plugin.class.js';
|
|
9
9
|
import { requirePluginDependency } from './concerns/plugin-dependencies.js';
|
|
10
10
|
import tryFn from '../concerns/try-fn.js';
|
|
11
11
|
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
* @property {Object} models - Model configurations
|
|
30
30
|
* @property {boolean} [verbose=false] - Enable verbose logging
|
|
31
31
|
* @property {number} [minTrainingSamples=10] - Minimum samples required for training
|
|
32
|
+
* @property {boolean} [saveModel=true] - Save trained models to S3
|
|
33
|
+
* @property {boolean} [saveTrainingData=false] - Save intermediate training data to S3
|
|
32
34
|
*
|
|
33
35
|
* @example
|
|
34
36
|
* new MLPlugin({
|
|
@@ -38,9 +40,12 @@ import {
|
|
|
38
40
|
* resource: 'products',
|
|
39
41
|
* features: ['cost', 'margin', 'demand'],
|
|
40
42
|
* target: 'price',
|
|
43
|
+
* partition: { name: 'byCategory', values: { category: 'electronics' } }, // Optional
|
|
41
44
|
* autoTrain: true,
|
|
42
45
|
* trainInterval: 3600000, // 1 hour
|
|
43
46
|
* trainAfterInserts: 100,
|
|
47
|
+
* saveModel: true, // Save to S3 after training
|
|
48
|
+
* saveTrainingData: true, // Save prepared dataset
|
|
44
49
|
* modelConfig: {
|
|
45
50
|
* epochs: 50,
|
|
46
51
|
* batchSize: 32,
|
|
@@ -48,7 +53,9 @@ import {
|
|
|
48
53
|
* }
|
|
49
54
|
* }
|
|
50
55
|
* },
|
|
51
|
-
* verbose: true
|
|
56
|
+
* verbose: true,
|
|
57
|
+
* saveModel: true,
|
|
58
|
+
* saveTrainingData: false
|
|
52
59
|
* })
|
|
53
60
|
*/
|
|
54
61
|
export class MLPlugin extends Plugin {
|
|
@@ -58,7 +65,10 @@ export class MLPlugin extends Plugin {
|
|
|
58
65
|
this.config = {
|
|
59
66
|
models: options.models || {},
|
|
60
67
|
verbose: options.verbose || false,
|
|
61
|
-
minTrainingSamples: options.minTrainingSamples || 10
|
|
68
|
+
minTrainingSamples: options.minTrainingSamples || 10,
|
|
69
|
+
saveModel: options.saveModel !== false, // Default true
|
|
70
|
+
saveTrainingData: options.saveTrainingData || false,
|
|
71
|
+
enableVersioning: options.enableVersioning !== false // Default true
|
|
62
72
|
};
|
|
63
73
|
|
|
64
74
|
// Validate TensorFlow.js dependency
|
|
@@ -70,6 +80,12 @@ export class MLPlugin extends Plugin {
|
|
|
70
80
|
// Model instances
|
|
71
81
|
this.models = {};
|
|
72
82
|
|
|
83
|
+
// Model versioning
|
|
84
|
+
this.modelVersions = new Map(); // Track versions per model: { currentVersion, latestVersion }
|
|
85
|
+
|
|
86
|
+
// Model cache for resource.predict()
|
|
87
|
+
this.modelCache = new Map(); // Cache: resourceName_attribute -> modelName
|
|
88
|
+
|
|
73
89
|
// Training state
|
|
74
90
|
this.training = new Map(); // Track ongoing training
|
|
75
91
|
this.insertCounters = new Map(); // Track inserts per resource
|
|
@@ -104,6 +120,12 @@ export class MLPlugin extends Plugin {
|
|
|
104
120
|
await this._initializeModel(modelName, modelConfig);
|
|
105
121
|
}
|
|
106
122
|
|
|
123
|
+
// Build model cache (resource -> attribute -> modelName mapping)
|
|
124
|
+
this._buildModelCache();
|
|
125
|
+
|
|
126
|
+
// Inject ML methods into Resource prototype
|
|
127
|
+
this._injectResourceMethods();
|
|
128
|
+
|
|
107
129
|
// Setup auto-training hooks
|
|
108
130
|
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
109
131
|
if (modelConfig.autoTrain) {
|
|
@@ -127,6 +149,13 @@ export class MLPlugin extends Plugin {
|
|
|
127
149
|
* Start the plugin
|
|
128
150
|
*/
|
|
129
151
|
async onStart() {
|
|
152
|
+
// Initialize versioning for each model
|
|
153
|
+
if (this.config.enableVersioning) {
|
|
154
|
+
for (const modelName of Object.keys(this.models)) {
|
|
155
|
+
await this._initializeVersioning(modelName);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
130
159
|
// Try to load previously trained models
|
|
131
160
|
for (const modelName of Object.keys(this.models)) {
|
|
132
161
|
await this._loadModel(modelName);
|
|
@@ -166,15 +195,172 @@ export class MLPlugin extends Plugin {
|
|
|
166
195
|
await this.onStop();
|
|
167
196
|
|
|
168
197
|
if (options.purgeData) {
|
|
169
|
-
// Delete all saved models from plugin storage
|
|
198
|
+
// Delete all saved models and training data from plugin storage
|
|
170
199
|
for (const modelName of Object.keys(this.models)) {
|
|
171
200
|
await this._deleteModel(modelName);
|
|
201
|
+
await this._deleteTrainingData(modelName);
|
|
172
202
|
}
|
|
173
203
|
|
|
174
204
|
if (this.config.verbose) {
|
|
175
|
-
console.log('[MLPlugin] Purged all model data');
|
|
205
|
+
console.log('[MLPlugin] Purged all model data and training data');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build model cache for fast lookup
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
_buildModelCache() {
|
|
215
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
216
|
+
const cacheKey = `${modelConfig.resource}_${modelConfig.target}`;
|
|
217
|
+
this.modelCache.set(cacheKey, modelName);
|
|
218
|
+
|
|
219
|
+
if (this.config.verbose) {
|
|
220
|
+
console.log(`[MLPlugin] Cached model "${modelName}" for ${modelConfig.resource}.predict(..., '${modelConfig.target}')`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Inject ML methods into Resource instances
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
_injectResourceMethods() {
|
|
230
|
+
const plugin = this;
|
|
231
|
+
|
|
232
|
+
// Store reference to plugin in database for resource access
|
|
233
|
+
if (!this.database._mlPlugin) {
|
|
234
|
+
this.database._mlPlugin = this;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Add predict() method to Resource prototype
|
|
238
|
+
if (!this.database.Resource.prototype.predict) {
|
|
239
|
+
this.database.Resource.prototype.predict = async function(input, targetAttribute) {
|
|
240
|
+
const mlPlugin = this.database._mlPlugin;
|
|
241
|
+
if (!mlPlugin) {
|
|
242
|
+
throw new Error('MLPlugin not installed');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return await mlPlugin._resourcePredict(this.name, input, targetAttribute);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Add trainModel() method to Resource prototype
|
|
250
|
+
if (!this.database.Resource.prototype.trainModel) {
|
|
251
|
+
this.database.Resource.prototype.trainModel = async function(targetAttribute, options = {}) {
|
|
252
|
+
const mlPlugin = this.database._mlPlugin;
|
|
253
|
+
if (!mlPlugin) {
|
|
254
|
+
throw new Error('MLPlugin not installed');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return await mlPlugin._resourceTrainModel(this.name, targetAttribute, options);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add listModels() method to Resource prototype
|
|
262
|
+
if (!this.database.Resource.prototype.listModels) {
|
|
263
|
+
this.database.Resource.prototype.listModels = function() {
|
|
264
|
+
const mlPlugin = this.database._mlPlugin;
|
|
265
|
+
if (!mlPlugin) {
|
|
266
|
+
throw new Error('MLPlugin not installed');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return mlPlugin._resourceListModels(this.name);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (this.config.verbose) {
|
|
274
|
+
console.log('[MLPlugin] Injected ML methods into Resource prototype');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Find model for a resource and target attribute
|
|
280
|
+
* @private
|
|
281
|
+
*/
|
|
282
|
+
_findModelForResource(resourceName, targetAttribute) {
|
|
283
|
+
const cacheKey = `${resourceName}_${targetAttribute}`;
|
|
284
|
+
|
|
285
|
+
// Try cache first
|
|
286
|
+
if (this.modelCache.has(cacheKey)) {
|
|
287
|
+
return this.modelCache.get(cacheKey);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Search through all models
|
|
291
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
292
|
+
if (modelConfig.resource === resourceName && modelConfig.target === targetAttribute) {
|
|
293
|
+
// Cache for next time
|
|
294
|
+
this.modelCache.set(cacheKey, modelName);
|
|
295
|
+
return modelName;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resource predict implementation
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
async _resourcePredict(resourceName, input, targetAttribute) {
|
|
307
|
+
const modelName = this._findModelForResource(resourceName, targetAttribute);
|
|
308
|
+
|
|
309
|
+
if (!modelName) {
|
|
310
|
+
throw new ModelNotFoundError(
|
|
311
|
+
`No model found for resource "${resourceName}" with target "${targetAttribute}"`,
|
|
312
|
+
{ resourceName, targetAttribute, availableModels: Object.keys(this.models) }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (this.config.verbose) {
|
|
317
|
+
console.log(`[MLPlugin] Resource prediction: ${resourceName}.predict(..., '${targetAttribute}') -> model "${modelName}"`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return await this.predict(modelName, input);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resource trainModel implementation
|
|
325
|
+
* @private
|
|
326
|
+
*/
|
|
327
|
+
async _resourceTrainModel(resourceName, targetAttribute, options = {}) {
|
|
328
|
+
const modelName = this._findModelForResource(resourceName, targetAttribute);
|
|
329
|
+
|
|
330
|
+
if (!modelName) {
|
|
331
|
+
throw new ModelNotFoundError(
|
|
332
|
+
`No model found for resource "${resourceName}" with target "${targetAttribute}"`,
|
|
333
|
+
{ resourceName, targetAttribute, availableModels: Object.keys(this.models) }
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (this.config.verbose) {
|
|
338
|
+
console.log(`[MLPlugin] Resource training: ${resourceName}.trainModel('${targetAttribute}') -> model "${modelName}"`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return await this.train(modelName, options);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* List models for a resource
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
_resourceListModels(resourceName) {
|
|
349
|
+
const models = [];
|
|
350
|
+
|
|
351
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
352
|
+
if (modelConfig.resource === resourceName) {
|
|
353
|
+
models.push({
|
|
354
|
+
name: modelName,
|
|
355
|
+
type: modelConfig.type,
|
|
356
|
+
target: modelConfig.target,
|
|
357
|
+
features: modelConfig.features,
|
|
358
|
+
isTrained: this.models[modelName]?.isTrained || false
|
|
359
|
+
});
|
|
176
360
|
}
|
|
177
361
|
}
|
|
362
|
+
|
|
363
|
+
return models;
|
|
178
364
|
}
|
|
179
365
|
|
|
180
366
|
/**
|
|
@@ -365,18 +551,44 @@ export class MLPlugin extends Plugin {
|
|
|
365
551
|
);
|
|
366
552
|
}
|
|
367
553
|
|
|
368
|
-
// Fetch training data
|
|
554
|
+
// Fetch training data (with optional partition filtering)
|
|
369
555
|
if (this.config.verbose) {
|
|
370
556
|
console.log(`[MLPlugin] Fetching training data for "${modelName}"...`);
|
|
371
557
|
}
|
|
372
558
|
|
|
373
|
-
|
|
559
|
+
let data;
|
|
560
|
+
const partition = modelConfig.partition;
|
|
374
561
|
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
562
|
+
if (partition && partition.name) {
|
|
563
|
+
// Use partition filtering
|
|
564
|
+
if (this.config.verbose) {
|
|
565
|
+
console.log(`[MLPlugin] Using partition "${partition.name}" with values:`, partition.values);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const [ok, err, partitionData] = await tryFn(() =>
|
|
569
|
+
resource.listPartition(partition.name, partition.values)
|
|
379
570
|
);
|
|
571
|
+
|
|
572
|
+
if (!ok) {
|
|
573
|
+
throw new TrainingError(
|
|
574
|
+
`Failed to fetch training data from partition: ${err.message}`,
|
|
575
|
+
{ modelName, resource: modelConfig.resource, partition: partition.name, originalError: err.message }
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
data = partitionData;
|
|
580
|
+
} else {
|
|
581
|
+
// Fetch all data
|
|
582
|
+
const [ok, err, allData] = await tryFn(() => resource.list());
|
|
583
|
+
|
|
584
|
+
if (!ok) {
|
|
585
|
+
throw new TrainingError(
|
|
586
|
+
`Failed to fetch training data: ${err.message}`,
|
|
587
|
+
{ modelName, resource: modelConfig.resource, originalError: err.message }
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
data = allData;
|
|
380
592
|
}
|
|
381
593
|
|
|
382
594
|
if (!data || data.length < this.config.minTrainingSamples) {
|
|
@@ -390,11 +602,26 @@ export class MLPlugin extends Plugin {
|
|
|
390
602
|
console.log(`[MLPlugin] Training "${modelName}" with ${data.length} samples...`);
|
|
391
603
|
}
|
|
392
604
|
|
|
605
|
+
// Save intermediate training data if enabled
|
|
606
|
+
const shouldSaveTrainingData = modelConfig.saveTrainingData !== undefined
|
|
607
|
+
? modelConfig.saveTrainingData
|
|
608
|
+
: this.config.saveTrainingData;
|
|
609
|
+
|
|
610
|
+
if (shouldSaveTrainingData) {
|
|
611
|
+
await this._saveTrainingData(modelName, data);
|
|
612
|
+
}
|
|
613
|
+
|
|
393
614
|
// Train model
|
|
394
615
|
const result = await model.train(data);
|
|
395
616
|
|
|
396
|
-
// Save model to plugin storage
|
|
397
|
-
|
|
617
|
+
// Save model to plugin storage if enabled
|
|
618
|
+
const shouldSaveModel = modelConfig.saveModel !== undefined
|
|
619
|
+
? modelConfig.saveModel
|
|
620
|
+
: this.config.saveModel;
|
|
621
|
+
|
|
622
|
+
if (shouldSaveModel) {
|
|
623
|
+
await this._saveModel(modelName);
|
|
624
|
+
}
|
|
398
625
|
|
|
399
626
|
this.stats.totalTrainings++;
|
|
400
627
|
|
|
@@ -573,6 +800,81 @@ export class MLPlugin extends Plugin {
|
|
|
573
800
|
}
|
|
574
801
|
}
|
|
575
802
|
|
|
803
|
+
/**
|
|
804
|
+
* Initialize versioning for a model
|
|
805
|
+
* @private
|
|
806
|
+
*/
|
|
807
|
+
async _initializeVersioning(modelName) {
|
|
808
|
+
try {
|
|
809
|
+
const storage = this.getStorage();
|
|
810
|
+
const [ok, err, versionInfo] = await tryFn(() => storage.get(`version_${modelName}`));
|
|
811
|
+
|
|
812
|
+
if (ok && versionInfo) {
|
|
813
|
+
// Load existing version info
|
|
814
|
+
this.modelVersions.set(modelName, {
|
|
815
|
+
currentVersion: versionInfo.currentVersion || 1,
|
|
816
|
+
latestVersion: versionInfo.latestVersion || 1
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
if (this.config.verbose) {
|
|
820
|
+
console.log(`[MLPlugin] Loaded version info for "${modelName}": v${versionInfo.currentVersion}`);
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
// Initialize new versioning
|
|
824
|
+
this.modelVersions.set(modelName, {
|
|
825
|
+
currentVersion: 1,
|
|
826
|
+
latestVersion: 0 // No versions yet
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
if (this.config.verbose) {
|
|
830
|
+
console.log(`[MLPlugin] Initialized versioning for "${modelName}"`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
console.error(`[MLPlugin] Failed to initialize versioning for "${modelName}":`, error.message);
|
|
835
|
+
// Fallback to v1
|
|
836
|
+
this.modelVersions.set(modelName, { currentVersion: 1, latestVersion: 0 });
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Get next version number for a model
|
|
842
|
+
* @private
|
|
843
|
+
*/
|
|
844
|
+
_getNextVersion(modelName) {
|
|
845
|
+
const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
|
|
846
|
+
return versionInfo.latestVersion + 1;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Update version info in storage
|
|
851
|
+
* @private
|
|
852
|
+
*/
|
|
853
|
+
async _updateVersionInfo(modelName, version) {
|
|
854
|
+
try {
|
|
855
|
+
const storage = this.getStorage();
|
|
856
|
+
const versionInfo = this.modelVersions.get(modelName) || { currentVersion: 1, latestVersion: 0 };
|
|
857
|
+
|
|
858
|
+
versionInfo.latestVersion = Math.max(versionInfo.latestVersion, version);
|
|
859
|
+
versionInfo.currentVersion = version; // Set new version as current
|
|
860
|
+
|
|
861
|
+
this.modelVersions.set(modelName, versionInfo);
|
|
862
|
+
|
|
863
|
+
await storage.patch(`version_${modelName}`, {
|
|
864
|
+
modelName,
|
|
865
|
+
currentVersion: versionInfo.currentVersion,
|
|
866
|
+
latestVersion: versionInfo.latestVersion,
|
|
867
|
+
updatedAt: new Date().toISOString()
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
if (this.config.verbose) {
|
|
871
|
+
console.log(`[MLPlugin] Updated version info for "${modelName}": current=v${versionInfo.currentVersion}, latest=v${versionInfo.latestVersion}`);
|
|
872
|
+
}
|
|
873
|
+
} catch (error) {
|
|
874
|
+
console.error(`[MLPlugin] Failed to update version info for "${modelName}":`, error.message);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
576
878
|
/**
|
|
577
879
|
* Save model to plugin storage
|
|
578
880
|
* @private
|
|
@@ -589,21 +891,145 @@ export class MLPlugin extends Plugin {
|
|
|
589
891
|
return;
|
|
590
892
|
}
|
|
591
893
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
894
|
+
const enableVersioning = this.config.enableVersioning;
|
|
895
|
+
|
|
896
|
+
if (enableVersioning) {
|
|
897
|
+
// Save with version
|
|
898
|
+
const version = this._getNextVersion(modelName);
|
|
899
|
+
const modelStats = this.models[modelName].getStats();
|
|
900
|
+
|
|
901
|
+
// Save versioned model
|
|
902
|
+
await storage.patch(`model_${modelName}_v${version}`, {
|
|
903
|
+
modelName,
|
|
904
|
+
version,
|
|
905
|
+
type: 'model',
|
|
906
|
+
data: JSON.stringify(exportedModel),
|
|
907
|
+
metrics: JSON.stringify({
|
|
908
|
+
loss: modelStats.loss,
|
|
909
|
+
accuracy: modelStats.accuracy,
|
|
910
|
+
samples: modelStats.samples
|
|
911
|
+
}),
|
|
912
|
+
savedAt: new Date().toISOString()
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// Update version info
|
|
916
|
+
await this._updateVersionInfo(modelName, version);
|
|
917
|
+
|
|
918
|
+
// Save active reference (points to current version)
|
|
919
|
+
await storage.patch(`model_${modelName}_active`, {
|
|
920
|
+
modelName,
|
|
921
|
+
version,
|
|
922
|
+
type: 'reference',
|
|
923
|
+
updatedAt: new Date().toISOString()
|
|
924
|
+
});
|
|
598
925
|
|
|
599
|
-
|
|
600
|
-
|
|
926
|
+
if (this.config.verbose) {
|
|
927
|
+
console.log(`[MLPlugin] Saved model "${modelName}" v${version} to plugin storage (S3)`);
|
|
928
|
+
}
|
|
929
|
+
} else {
|
|
930
|
+
// Save without versioning (legacy behavior)
|
|
931
|
+
await storage.patch(`model_${modelName}`, {
|
|
932
|
+
modelName,
|
|
933
|
+
type: 'model',
|
|
934
|
+
data: JSON.stringify(exportedModel),
|
|
935
|
+
savedAt: new Date().toISOString()
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
if (this.config.verbose) {
|
|
939
|
+
console.log(`[MLPlugin] Saved model "${modelName}" to plugin storage (S3)`);
|
|
940
|
+
}
|
|
601
941
|
}
|
|
602
942
|
} catch (error) {
|
|
603
943
|
console.error(`[MLPlugin] Failed to save model "${modelName}":`, error.message);
|
|
604
944
|
}
|
|
605
945
|
}
|
|
606
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Save intermediate training data to plugin storage (incremental)
|
|
949
|
+
* @private
|
|
950
|
+
*/
|
|
951
|
+
async _saveTrainingData(modelName, rawData) {
|
|
952
|
+
try {
|
|
953
|
+
const storage = this.getStorage();
|
|
954
|
+
const model = this.models[modelName];
|
|
955
|
+
const modelConfig = this.config.models[modelName];
|
|
956
|
+
const modelStats = model.getStats();
|
|
957
|
+
const enableVersioning = this.config.enableVersioning;
|
|
958
|
+
|
|
959
|
+
// Extract features and target from raw data
|
|
960
|
+
const trainingEntry = {
|
|
961
|
+
version: enableVersioning ? this.modelVersions.get(modelName)?.latestVersion || 1 : undefined,
|
|
962
|
+
samples: rawData.length,
|
|
963
|
+
features: modelConfig.features,
|
|
964
|
+
target: modelConfig.target,
|
|
965
|
+
data: rawData.map(item => {
|
|
966
|
+
const features = {};
|
|
967
|
+
modelConfig.features.forEach(feature => {
|
|
968
|
+
features[feature] = item[feature];
|
|
969
|
+
});
|
|
970
|
+
return {
|
|
971
|
+
features,
|
|
972
|
+
target: item[modelConfig.target]
|
|
973
|
+
};
|
|
974
|
+
}),
|
|
975
|
+
metrics: {
|
|
976
|
+
loss: modelStats.loss,
|
|
977
|
+
accuracy: modelStats.accuracy,
|
|
978
|
+
r2: modelStats.r2
|
|
979
|
+
},
|
|
980
|
+
trainedAt: new Date().toISOString()
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
if (enableVersioning) {
|
|
984
|
+
// Incremental: Load existing history and append
|
|
985
|
+
const [ok, err, existing] = await tryFn(() => storage.get(`training_history_${modelName}`));
|
|
986
|
+
|
|
987
|
+
let history = [];
|
|
988
|
+
if (ok && existing && existing.history) {
|
|
989
|
+
try {
|
|
990
|
+
history = JSON.parse(existing.history);
|
|
991
|
+
} catch (e) {
|
|
992
|
+
history = [];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Append new entry
|
|
997
|
+
history.push(trainingEntry);
|
|
998
|
+
|
|
999
|
+
// Save updated history
|
|
1000
|
+
await storage.patch(`training_history_${modelName}`, {
|
|
1001
|
+
modelName,
|
|
1002
|
+
type: 'training_history',
|
|
1003
|
+
totalTrainings: history.length,
|
|
1004
|
+
latestVersion: trainingEntry.version,
|
|
1005
|
+
history: JSON.stringify(history),
|
|
1006
|
+
updatedAt: new Date().toISOString()
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
if (this.config.verbose) {
|
|
1010
|
+
console.log(`[MLPlugin] Appended training data for "${modelName}" v${trainingEntry.version} (${trainingEntry.samples} samples, total: ${history.length} trainings)`);
|
|
1011
|
+
}
|
|
1012
|
+
} else {
|
|
1013
|
+
// Legacy: Replace training data (non-incremental)
|
|
1014
|
+
await storage.patch(`training_data_${modelName}`, {
|
|
1015
|
+
modelName,
|
|
1016
|
+
type: 'training_data',
|
|
1017
|
+
samples: trainingEntry.samples,
|
|
1018
|
+
features: JSON.stringify(trainingEntry.features),
|
|
1019
|
+
target: trainingEntry.target,
|
|
1020
|
+
data: JSON.stringify(trainingEntry.data),
|
|
1021
|
+
savedAt: trainingEntry.trainedAt
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
if (this.config.verbose) {
|
|
1025
|
+
console.log(`[MLPlugin] Saved training data for "${modelName}" (${trainingEntry.samples} samples) to plugin storage (S3)`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
console.error(`[MLPlugin] Failed to save training data for "${modelName}":`, error.message);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
607
1033
|
/**
|
|
608
1034
|
* Load model from plugin storage
|
|
609
1035
|
* @private
|
|
@@ -611,26 +1037,102 @@ export class MLPlugin extends Plugin {
|
|
|
611
1037
|
async _loadModel(modelName) {
|
|
612
1038
|
try {
|
|
613
1039
|
const storage = this.getStorage();
|
|
614
|
-
const
|
|
1040
|
+
const enableVersioning = this.config.enableVersioning;
|
|
1041
|
+
|
|
1042
|
+
if (enableVersioning) {
|
|
1043
|
+
// Load active version
|
|
1044
|
+
const [okRef, errRef, activeRef] = await tryFn(() => storage.get(`model_${modelName}_active`));
|
|
1045
|
+
|
|
1046
|
+
if (okRef && activeRef && activeRef.version) {
|
|
1047
|
+
// Load the active version
|
|
1048
|
+
const version = activeRef.version;
|
|
1049
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${version}`));
|
|
1050
|
+
|
|
1051
|
+
if (ok && versionData) {
|
|
1052
|
+
const modelData = JSON.parse(versionData.data);
|
|
1053
|
+
await this.models[modelName].import(modelData);
|
|
1054
|
+
|
|
1055
|
+
if (this.config.verbose) {
|
|
1056
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (active) from plugin storage (S3)`);
|
|
1057
|
+
}
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// No active reference, try to load latest version
|
|
1063
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
1064
|
+
if (versionInfo && versionInfo.latestVersion > 0) {
|
|
1065
|
+
const version = versionInfo.latestVersion;
|
|
1066
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${version}`));
|
|
1067
|
+
|
|
1068
|
+
if (ok && versionData) {
|
|
1069
|
+
const modelData = JSON.parse(versionData.data);
|
|
1070
|
+
await this.models[modelName].import(modelData);
|
|
1071
|
+
|
|
1072
|
+
if (this.config.verbose) {
|
|
1073
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (latest) from plugin storage (S3)`);
|
|
1074
|
+
}
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
615
1078
|
|
|
616
|
-
if (!ok || !record) {
|
|
617
1079
|
if (this.config.verbose) {
|
|
618
|
-
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
1080
|
+
console.log(`[MLPlugin] No saved model versions found for "${modelName}"`);
|
|
619
1081
|
}
|
|
620
|
-
|
|
621
|
-
|
|
1082
|
+
} else {
|
|
1083
|
+
// Legacy: Load non-versioned model
|
|
1084
|
+
const [ok, err, record] = await tryFn(() => storage.get(`model_${modelName}`));
|
|
622
1085
|
|
|
623
|
-
|
|
624
|
-
|
|
1086
|
+
if (!ok || !record) {
|
|
1087
|
+
if (this.config.verbose) {
|
|
1088
|
+
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
1089
|
+
}
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
625
1092
|
|
|
626
|
-
|
|
627
|
-
|
|
1093
|
+
const modelData = JSON.parse(record.data);
|
|
1094
|
+
await this.models[modelName].import(modelData);
|
|
1095
|
+
|
|
1096
|
+
if (this.config.verbose) {
|
|
1097
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" from plugin storage (S3)`);
|
|
1098
|
+
}
|
|
628
1099
|
}
|
|
629
1100
|
} catch (error) {
|
|
630
1101
|
console.error(`[MLPlugin] Failed to load model "${modelName}":`, error.message);
|
|
631
1102
|
}
|
|
632
1103
|
}
|
|
633
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* Load training data from plugin storage
|
|
1107
|
+
* @param {string} modelName - Model name
|
|
1108
|
+
* @returns {Object|null} Training data or null if not found
|
|
1109
|
+
*/
|
|
1110
|
+
async getTrainingData(modelName) {
|
|
1111
|
+
try {
|
|
1112
|
+
const storage = this.getStorage();
|
|
1113
|
+
const [ok, err, record] = await tryFn(() => storage.get(`training_data_${modelName}`));
|
|
1114
|
+
|
|
1115
|
+
if (!ok || !record) {
|
|
1116
|
+
if (this.config.verbose) {
|
|
1117
|
+
console.log(`[MLPlugin] No saved training data found for "${modelName}"`);
|
|
1118
|
+
}
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return {
|
|
1123
|
+
modelName: record.modelName,
|
|
1124
|
+
samples: record.samples,
|
|
1125
|
+
features: JSON.parse(record.features),
|
|
1126
|
+
target: record.target,
|
|
1127
|
+
data: JSON.parse(record.data),
|
|
1128
|
+
savedAt: record.savedAt
|
|
1129
|
+
};
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
console.error(`[MLPlugin] Failed to load training data for "${modelName}":`, error.message);
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
634
1136
|
/**
|
|
635
1137
|
* Delete model from plugin storage
|
|
636
1138
|
* @private
|
|
@@ -650,6 +1152,266 @@ export class MLPlugin extends Plugin {
|
|
|
650
1152
|
}
|
|
651
1153
|
}
|
|
652
1154
|
}
|
|
653
|
-
}
|
|
654
1155
|
|
|
655
|
-
|
|
1156
|
+
/**
|
|
1157
|
+
* Delete training data from plugin storage
|
|
1158
|
+
* @private
|
|
1159
|
+
*/
|
|
1160
|
+
async _deleteTrainingData(modelName) {
|
|
1161
|
+
try {
|
|
1162
|
+
const storage = this.getStorage();
|
|
1163
|
+
await storage.delete(`training_data_${modelName}`);
|
|
1164
|
+
|
|
1165
|
+
if (this.config.verbose) {
|
|
1166
|
+
console.log(`[MLPlugin] Deleted training data for "${modelName}" from plugin storage`);
|
|
1167
|
+
}
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
// Ignore errors (training data might not exist)
|
|
1170
|
+
if (this.config.verbose) {
|
|
1171
|
+
console.log(`[MLPlugin] Could not delete training data "${modelName}": ${error.message}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* List all versions of a model
|
|
1178
|
+
* @param {string} modelName - Model name
|
|
1179
|
+
* @returns {Array} List of version info
|
|
1180
|
+
*/
|
|
1181
|
+
async listModelVersions(modelName) {
|
|
1182
|
+
if (!this.config.enableVersioning) {
|
|
1183
|
+
throw new MLError('Versioning is not enabled', { modelName });
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
try {
|
|
1187
|
+
const storage = this.getStorage();
|
|
1188
|
+
const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
|
|
1189
|
+
const versions = [];
|
|
1190
|
+
|
|
1191
|
+
// Load each version
|
|
1192
|
+
for (let v = 1; v <= versionInfo.latestVersion; v++) {
|
|
1193
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${v}`));
|
|
1194
|
+
|
|
1195
|
+
if (ok && versionData) {
|
|
1196
|
+
const metrics = versionData.metrics ? JSON.parse(versionData.metrics) : {};
|
|
1197
|
+
versions.push({
|
|
1198
|
+
version: v,
|
|
1199
|
+
savedAt: versionData.savedAt,
|
|
1200
|
+
isCurrent: v === versionInfo.currentVersion,
|
|
1201
|
+
metrics
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
return versions;
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
console.error(`[MLPlugin] Failed to list versions for "${modelName}":`, error.message);
|
|
1209
|
+
return [];
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Load a specific version of a model
|
|
1215
|
+
* @param {string} modelName - Model name
|
|
1216
|
+
* @param {number} version - Version number
|
|
1217
|
+
*/
|
|
1218
|
+
async loadModelVersion(modelName, version) {
|
|
1219
|
+
if (!this.config.enableVersioning) {
|
|
1220
|
+
throw new MLError('Versioning is not enabled', { modelName });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (!this.models[modelName]) {
|
|
1224
|
+
throw new ModelNotFoundError(`Model "${modelName}" not found`, { modelName });
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
try {
|
|
1228
|
+
const storage = this.getStorage();
|
|
1229
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${version}`));
|
|
1230
|
+
|
|
1231
|
+
if (!ok || !versionData) {
|
|
1232
|
+
throw new MLError(`Version ${version} not found for model "${modelName}"`, { modelName, version });
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const modelData = JSON.parse(versionData.data);
|
|
1236
|
+
await this.models[modelName].import(modelData);
|
|
1237
|
+
|
|
1238
|
+
// Update current version in memory (don't save to storage yet)
|
|
1239
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
1240
|
+
if (versionInfo) {
|
|
1241
|
+
versionInfo.currentVersion = version;
|
|
1242
|
+
this.modelVersions.set(modelName, versionInfo);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (this.config.verbose) {
|
|
1246
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version}`);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return {
|
|
1250
|
+
version,
|
|
1251
|
+
metrics: versionData.metrics ? JSON.parse(versionData.metrics) : {},
|
|
1252
|
+
savedAt: versionData.savedAt
|
|
1253
|
+
};
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
console.error(`[MLPlugin] Failed to load version ${version} for "${modelName}":`, error.message);
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Set active version for a model (used for predictions)
|
|
1262
|
+
* @param {string} modelName - Model name
|
|
1263
|
+
* @param {number} version - Version number
|
|
1264
|
+
*/
|
|
1265
|
+
async setActiveVersion(modelName, version) {
|
|
1266
|
+
if (!this.config.enableVersioning) {
|
|
1267
|
+
throw new MLError('Versioning is not enabled', { modelName });
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Load the version into the model
|
|
1271
|
+
await this.loadModelVersion(modelName, version);
|
|
1272
|
+
|
|
1273
|
+
// Update version info in storage
|
|
1274
|
+
await this._updateVersionInfo(modelName, version);
|
|
1275
|
+
|
|
1276
|
+
// Update active reference
|
|
1277
|
+
const storage = this.getStorage();
|
|
1278
|
+
await storage.patch(`model_${modelName}_active`, {
|
|
1279
|
+
modelName,
|
|
1280
|
+
version,
|
|
1281
|
+
type: 'reference',
|
|
1282
|
+
updatedAt: new Date().toISOString()
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (this.config.verbose) {
|
|
1286
|
+
console.log(`[MLPlugin] Set model "${modelName}" active version to v${version}`);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
return { modelName, version };
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Get training history for a model
|
|
1294
|
+
* @param {string} modelName - Model name
|
|
1295
|
+
* @returns {Array} Training history
|
|
1296
|
+
*/
|
|
1297
|
+
async getTrainingHistory(modelName) {
|
|
1298
|
+
if (!this.config.enableVersioning) {
|
|
1299
|
+
// Fallback to legacy getTrainingData
|
|
1300
|
+
return await this.getTrainingData(modelName);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
try {
|
|
1304
|
+
const storage = this.getStorage();
|
|
1305
|
+
const [ok, err, historyData] = await tryFn(() => storage.get(`training_history_${modelName}`));
|
|
1306
|
+
|
|
1307
|
+
if (!ok || !historyData) {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return {
|
|
1312
|
+
modelName: historyData.modelName,
|
|
1313
|
+
totalTrainings: historyData.totalTrainings,
|
|
1314
|
+
latestVersion: historyData.latestVersion,
|
|
1315
|
+
history: JSON.parse(historyData.history),
|
|
1316
|
+
updatedAt: historyData.updatedAt
|
|
1317
|
+
};
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
console.error(`[MLPlugin] Failed to load training history for "${modelName}":`, error.message);
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Compare metrics between two versions
|
|
1326
|
+
* @param {string} modelName - Model name
|
|
1327
|
+
* @param {number} version1 - First version
|
|
1328
|
+
* @param {number} version2 - Second version
|
|
1329
|
+
* @returns {Object} Comparison results
|
|
1330
|
+
*/
|
|
1331
|
+
async compareVersions(modelName, version1, version2) {
|
|
1332
|
+
if (!this.config.enableVersioning) {
|
|
1333
|
+
throw new MLError('Versioning is not enabled', { modelName });
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
const storage = this.getStorage();
|
|
1338
|
+
|
|
1339
|
+
const [ok1, err1, v1Data] = await tryFn(() => storage.get(`model_${modelName}_v${version1}`));
|
|
1340
|
+
const [ok2, err2, v2Data] = await tryFn(() => storage.get(`model_${modelName}_v${version2}`));
|
|
1341
|
+
|
|
1342
|
+
if (!ok1 || !v1Data) {
|
|
1343
|
+
throw new MLError(`Version ${version1} not found`, { modelName, version: version1 });
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (!ok2 || !v2Data) {
|
|
1347
|
+
throw new MLError(`Version ${version2} not found`, { modelName, version: version2 });
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const metrics1 = v1Data.metrics ? JSON.parse(v1Data.metrics) : {};
|
|
1351
|
+
const metrics2 = v2Data.metrics ? JSON.parse(v2Data.metrics) : {};
|
|
1352
|
+
|
|
1353
|
+
return {
|
|
1354
|
+
modelName,
|
|
1355
|
+
version1: {
|
|
1356
|
+
version: version1,
|
|
1357
|
+
savedAt: v1Data.savedAt,
|
|
1358
|
+
metrics: metrics1
|
|
1359
|
+
},
|
|
1360
|
+
version2: {
|
|
1361
|
+
version: version2,
|
|
1362
|
+
savedAt: v2Data.savedAt,
|
|
1363
|
+
metrics: metrics2
|
|
1364
|
+
},
|
|
1365
|
+
improvement: {
|
|
1366
|
+
loss: metrics1.loss && metrics2.loss ? ((metrics1.loss - metrics2.loss) / metrics1.loss * 100).toFixed(2) + '%' : 'N/A',
|
|
1367
|
+
accuracy: metrics1.accuracy && metrics2.accuracy ? ((metrics2.accuracy - metrics1.accuracy) / metrics1.accuracy * 100).toFixed(2) + '%' : 'N/A'
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
console.error(`[MLPlugin] Failed to compare versions for "${modelName}":`, error.message);
|
|
1372
|
+
throw error;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Rollback to a previous version
|
|
1378
|
+
* @param {string} modelName - Model name
|
|
1379
|
+
* @param {number} version - Version to rollback to (defaults to previous version)
|
|
1380
|
+
* @returns {Object} Rollback info
|
|
1381
|
+
*/
|
|
1382
|
+
async rollbackVersion(modelName, version = null) {
|
|
1383
|
+
if (!this.config.enableVersioning) {
|
|
1384
|
+
throw new MLError('Versioning is not enabled', { modelName });
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
1388
|
+
if (!versionInfo) {
|
|
1389
|
+
throw new MLError(`No version info found for model "${modelName}"`, { modelName });
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// If no version specified, rollback to previous
|
|
1393
|
+
const targetVersion = version !== null ? version : Math.max(1, versionInfo.currentVersion - 1);
|
|
1394
|
+
|
|
1395
|
+
if (targetVersion === versionInfo.currentVersion) {
|
|
1396
|
+
throw new MLError('Cannot rollback to the same version', { modelName, version: targetVersion });
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (targetVersion < 1 || targetVersion > versionInfo.latestVersion) {
|
|
1400
|
+
throw new MLError(`Invalid version ${targetVersion}`, { modelName, version: targetVersion, latestVersion: versionInfo.latestVersion });
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Load and set as active
|
|
1404
|
+
const result = await this.setActiveVersion(modelName, targetVersion);
|
|
1405
|
+
|
|
1406
|
+
if (this.config.verbose) {
|
|
1407
|
+
console.log(`[MLPlugin] Rolled back model "${modelName}" from v${versionInfo.currentVersion} to v${targetVersion}`);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
return {
|
|
1411
|
+
modelName,
|
|
1412
|
+
previousVersion: versionInfo.currentVersion,
|
|
1413
|
+
currentVersion: targetVersion,
|
|
1414
|
+
...result
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|