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.
@@ -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
- const [ok, err, data] = await tryFn(() => resource.list());
559
+ let data;
560
+ const partition = modelConfig.partition;
374
561
 
375
- if (!ok) {
376
- throw new TrainingError(
377
- `Failed to fetch training data: ${err.message}`,
378
- { modelName, resource: modelConfig.resource, originalError: err.message }
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
- await this._saveModel(modelName);
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
- // 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
- });
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
- if (this.config.verbose) {
600
- console.log(`[MLPlugin] Saved model "${modelName}" to plugin storage`);
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 [ok, err, record] = await tryFn(() => storage.get(`model_${modelName}`));
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
- return;
621
- }
1082
+ } else {
1083
+ // Legacy: Load non-versioned model
1084
+ const [ok, err, record] = await tryFn(() => storage.get(`model_${modelName}`));
622
1085
 
623
- const modelData = JSON.parse(record.data);
624
- await this.models[modelName].import(modelData);
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
- if (this.config.verbose) {
627
- console.log(`[MLPlugin] Loaded model "${modelName}" from plugin storage`);
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
- export default MLPlugin;
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
+ }