s3db.js 13.1.0 → 13.2.2
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/README.md +9 -9
- package/dist/s3db.cjs.js +1249 -271
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1249 -271
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -1
- package/src/clients/memory-client.class.js +16 -16
- package/src/clients/s3-client.class.js +17 -17
- package/src/concerns/error-classifier.js +204 -0
- package/src/database.class.js +9 -9
- package/src/plugins/backup.plugin.js +8 -8
- package/src/plugins/cache.plugin.js +3 -3
- package/src/plugins/concerns/plugin-dependencies.js +12 -0
- package/src/plugins/geo.plugin.js +2 -2
- package/src/plugins/ml.plugin.js +337 -137
- package/src/plugins/relation.plugin.js +1 -1
- package/src/plugins/replicator.plugin.js +16 -16
- package/src/plugins/s3-queue.plugin.js +5 -5
- package/src/plugins/scheduler.plugin.js +7 -7
- package/src/plugins/state-machine.errors.js +9 -1
- package/src/plugins/state-machine.plugin.js +671 -16
- package/src/plugins/ttl.plugin.js +4 -4
- package/src/plugins/vector.plugin.js +10 -10
- package/src/resource.class.js +189 -40
package/src/plugins/ml.plugin.js
CHANGED
|
@@ -72,10 +72,7 @@ export class MLPlugin extends Plugin {
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
// Validate TensorFlow.js dependency
|
|
75
|
-
requirePluginDependency('
|
|
76
|
-
installCommand: 'pnpm add @tensorflow/tfjs-node',
|
|
77
|
-
reason: 'Required for machine learning model training and inference'
|
|
78
|
-
});
|
|
75
|
+
requirePluginDependency('ml-plugin');
|
|
79
76
|
|
|
80
77
|
// Model instances
|
|
81
78
|
this.models = {};
|
|
@@ -139,7 +136,7 @@ export class MLPlugin extends Plugin {
|
|
|
139
136
|
console.log(`[MLPlugin] Installed with ${Object.keys(this.models).length} models`);
|
|
140
137
|
}
|
|
141
138
|
|
|
142
|
-
this.emit('installed', {
|
|
139
|
+
this.emit('db:plugin:installed', {
|
|
143
140
|
plugin: 'MLPlugin',
|
|
144
141
|
models: Object.keys(this.models)
|
|
145
142
|
});
|
|
@@ -591,6 +588,29 @@ export class MLPlugin extends Plugin {
|
|
|
591
588
|
data = allData;
|
|
592
589
|
}
|
|
593
590
|
|
|
591
|
+
// Apply custom filter function if provided
|
|
592
|
+
if (modelConfig.filter && typeof modelConfig.filter === 'function') {
|
|
593
|
+
if (this.config.verbose) {
|
|
594
|
+
console.log(`[MLPlugin] Applying custom filter function...`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const originalLength = data.length;
|
|
598
|
+
data = data.filter(modelConfig.filter);
|
|
599
|
+
|
|
600
|
+
if (this.config.verbose) {
|
|
601
|
+
console.log(`[MLPlugin] Filter reduced dataset from ${originalLength} to ${data.length} samples`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Apply custom map function if provided
|
|
606
|
+
if (modelConfig.map && typeof modelConfig.map === 'function') {
|
|
607
|
+
if (this.config.verbose) {
|
|
608
|
+
console.log(`[MLPlugin] Applying custom map function...`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
data = data.map(modelConfig.map);
|
|
612
|
+
}
|
|
613
|
+
|
|
594
614
|
if (!data || data.length < this.config.minTrainingSamples) {
|
|
595
615
|
throw new TrainingError(
|
|
596
616
|
`Insufficient training data: ${data?.length || 0} samples (minimum: ${this.config.minTrainingSamples})`,
|
|
@@ -629,7 +649,7 @@ export class MLPlugin extends Plugin {
|
|
|
629
649
|
console.log(`[MLPlugin] Training completed for "${modelName}":`, result);
|
|
630
650
|
}
|
|
631
651
|
|
|
632
|
-
this.emit('
|
|
652
|
+
this.emit('plg:ml:model-trained', {
|
|
633
653
|
modelName,
|
|
634
654
|
type: modelConfig.type,
|
|
635
655
|
result
|
|
@@ -671,7 +691,7 @@ export class MLPlugin extends Plugin {
|
|
|
671
691
|
const result = await model.predict(input);
|
|
672
692
|
this.stats.totalPredictions++;
|
|
673
693
|
|
|
674
|
-
this.emit('prediction', {
|
|
694
|
+
this.emit('plg:ml:prediction', {
|
|
675
695
|
modelName,
|
|
676
696
|
input,
|
|
677
697
|
result
|
|
@@ -807,7 +827,11 @@ export class MLPlugin extends Plugin {
|
|
|
807
827
|
async _initializeVersioning(modelName) {
|
|
808
828
|
try {
|
|
809
829
|
const storage = this.getStorage();
|
|
810
|
-
const
|
|
830
|
+
const modelConfig = this.config.models[modelName];
|
|
831
|
+
const resourceName = modelConfig.resource;
|
|
832
|
+
const [ok, err, versionInfo] = await tryFn(() =>
|
|
833
|
+
storage.get(storage.getPluginKey(resourceName, 'metadata', modelName, 'versions'))
|
|
834
|
+
);
|
|
811
835
|
|
|
812
836
|
if (ok && versionInfo) {
|
|
813
837
|
// Load existing version info
|
|
@@ -853,6 +877,8 @@ export class MLPlugin extends Plugin {
|
|
|
853
877
|
async _updateVersionInfo(modelName, version) {
|
|
854
878
|
try {
|
|
855
879
|
const storage = this.getStorage();
|
|
880
|
+
const modelConfig = this.config.models[modelName];
|
|
881
|
+
const resourceName = modelConfig.resource;
|
|
856
882
|
const versionInfo = this.modelVersions.get(modelName) || { currentVersion: 1, latestVersion: 0 };
|
|
857
883
|
|
|
858
884
|
versionInfo.latestVersion = Math.max(versionInfo.latestVersion, version);
|
|
@@ -860,12 +886,16 @@ export class MLPlugin extends Plugin {
|
|
|
860
886
|
|
|
861
887
|
this.modelVersions.set(modelName, versionInfo);
|
|
862
888
|
|
|
863
|
-
await storage.
|
|
864
|
-
modelName,
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
889
|
+
await storage.set(
|
|
890
|
+
storage.getPluginKey(resourceName, 'metadata', modelName, 'versions'),
|
|
891
|
+
{
|
|
892
|
+
modelName,
|
|
893
|
+
currentVersion: versionInfo.currentVersion,
|
|
894
|
+
latestVersion: versionInfo.latestVersion,
|
|
895
|
+
updatedAt: new Date().toISOString()
|
|
896
|
+
},
|
|
897
|
+
{ behavior: 'body-overflow' }
|
|
898
|
+
);
|
|
869
899
|
|
|
870
900
|
if (this.config.verbose) {
|
|
871
901
|
console.log(`[MLPlugin] Updated version info for "${modelName}": current=v${versionInfo.currentVersion}, latest=v${versionInfo.latestVersion}`);
|
|
@@ -882,6 +912,8 @@ export class MLPlugin extends Plugin {
|
|
|
882
912
|
async _saveModel(modelName) {
|
|
883
913
|
try {
|
|
884
914
|
const storage = this.getStorage();
|
|
915
|
+
const modelConfig = this.config.models[modelName];
|
|
916
|
+
const resourceName = modelConfig.resource;
|
|
885
917
|
const exportedModel = await this.models[modelName].export();
|
|
886
918
|
|
|
887
919
|
if (!exportedModel) {
|
|
@@ -898,45 +930,57 @@ export class MLPlugin extends Plugin {
|
|
|
898
930
|
const version = this._getNextVersion(modelName);
|
|
899
931
|
const modelStats = this.models[modelName].getStats();
|
|
900
932
|
|
|
901
|
-
// Save versioned model
|
|
902
|
-
await storage.
|
|
903
|
-
modelName,
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
933
|
+
// Save versioned model binary to S3 body
|
|
934
|
+
await storage.set(
|
|
935
|
+
storage.getPluginKey(resourceName, 'models', modelName, `v${version}`),
|
|
936
|
+
{
|
|
937
|
+
modelName,
|
|
938
|
+
version,
|
|
939
|
+
type: 'model',
|
|
940
|
+
modelData: exportedModel, // TensorFlow.js model object (will go to body)
|
|
941
|
+
metrics: {
|
|
942
|
+
loss: modelStats.loss,
|
|
943
|
+
accuracy: modelStats.accuracy,
|
|
944
|
+
samples: modelStats.samples
|
|
945
|
+
},
|
|
946
|
+
savedAt: new Date().toISOString()
|
|
947
|
+
},
|
|
948
|
+
{ behavior: 'body-only' } // Large binary data goes to S3 body
|
|
949
|
+
);
|
|
914
950
|
|
|
915
951
|
// Update version info
|
|
916
952
|
await this._updateVersionInfo(modelName, version);
|
|
917
953
|
|
|
918
954
|
// Save active reference (points to current version)
|
|
919
|
-
await storage.
|
|
920
|
-
modelName,
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
955
|
+
await storage.set(
|
|
956
|
+
storage.getPluginKey(resourceName, 'metadata', modelName, 'active'),
|
|
957
|
+
{
|
|
958
|
+
modelName,
|
|
959
|
+
version,
|
|
960
|
+
type: 'reference',
|
|
961
|
+
updatedAt: new Date().toISOString()
|
|
962
|
+
},
|
|
963
|
+
{ behavior: 'body-overflow' } // Small metadata
|
|
964
|
+
);
|
|
925
965
|
|
|
926
966
|
if (this.config.verbose) {
|
|
927
|
-
console.log(`[MLPlugin] Saved model "${modelName}" v${version} to
|
|
967
|
+
console.log(`[MLPlugin] Saved model "${modelName}" v${version} to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
|
|
928
968
|
}
|
|
929
969
|
} else {
|
|
930
970
|
// Save without versioning (legacy behavior)
|
|
931
|
-
await storage.
|
|
932
|
-
modelName,
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
971
|
+
await storage.set(
|
|
972
|
+
storage.getPluginKey(resourceName, 'models', modelName, 'latest'),
|
|
973
|
+
{
|
|
974
|
+
modelName,
|
|
975
|
+
type: 'model',
|
|
976
|
+
modelData: exportedModel,
|
|
977
|
+
savedAt: new Date().toISOString()
|
|
978
|
+
},
|
|
979
|
+
{ behavior: 'body-only' }
|
|
980
|
+
);
|
|
937
981
|
|
|
938
982
|
if (this.config.verbose) {
|
|
939
|
-
console.log(`[MLPlugin] Saved model "${modelName}" to
|
|
983
|
+
console.log(`[MLPlugin] Saved model "${modelName}" to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
|
|
940
984
|
}
|
|
941
985
|
}
|
|
942
986
|
} catch (error) {
|
|
@@ -945,7 +989,7 @@ export class MLPlugin extends Plugin {
|
|
|
945
989
|
}
|
|
946
990
|
|
|
947
991
|
/**
|
|
948
|
-
* Save intermediate training data to plugin storage (incremental)
|
|
992
|
+
* Save intermediate training data to plugin storage (incremental - only new samples)
|
|
949
993
|
* @private
|
|
950
994
|
*/
|
|
951
995
|
async _saveTrainingData(modelName, rawData) {
|
|
@@ -953,76 +997,117 @@ export class MLPlugin extends Plugin {
|
|
|
953
997
|
const storage = this.getStorage();
|
|
954
998
|
const model = this.models[modelName];
|
|
955
999
|
const modelConfig = this.config.models[modelName];
|
|
1000
|
+
const resourceName = modelConfig.resource;
|
|
956
1001
|
const modelStats = model.getStats();
|
|
957
1002
|
const enableVersioning = this.config.enableVersioning;
|
|
958
1003
|
|
|
959
1004
|
// Extract features and target from raw data
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
};
|
|
1005
|
+
const processedData = rawData.map(item => {
|
|
1006
|
+
const features = {};
|
|
1007
|
+
modelConfig.features.forEach(feature => {
|
|
1008
|
+
features[feature] = item[feature];
|
|
1009
|
+
});
|
|
1010
|
+
return {
|
|
1011
|
+
id: item.id || `${Date.now()}_${Math.random()}`, // Use record ID or generate
|
|
1012
|
+
features,
|
|
1013
|
+
target: item[modelConfig.target]
|
|
1014
|
+
};
|
|
1015
|
+
});
|
|
982
1016
|
|
|
983
1017
|
if (enableVersioning) {
|
|
984
|
-
|
|
985
|
-
|
|
1018
|
+
const version = this._getNextVersion(modelName);
|
|
1019
|
+
|
|
1020
|
+
// Load existing history to calculate incremental data
|
|
1021
|
+
const [ok, err, existing] = await tryFn(() =>
|
|
1022
|
+
storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName))
|
|
1023
|
+
);
|
|
986
1024
|
|
|
987
1025
|
let history = [];
|
|
1026
|
+
let previousSampleIds = new Set();
|
|
1027
|
+
|
|
988
1028
|
if (ok && existing && existing.history) {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1029
|
+
history = existing.history;
|
|
1030
|
+
// Collect all IDs from previous versions
|
|
1031
|
+
history.forEach(entry => {
|
|
1032
|
+
if (entry.sampleIds) {
|
|
1033
|
+
entry.sampleIds.forEach(id => previousSampleIds.add(id));
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
994
1036
|
}
|
|
995
1037
|
|
|
996
|
-
//
|
|
997
|
-
|
|
1038
|
+
// Detect new samples (not in previous versions)
|
|
1039
|
+
const currentSampleIds = new Set(processedData.map(d => d.id));
|
|
1040
|
+
const newSamples = processedData.filter(d => !previousSampleIds.has(d.id));
|
|
1041
|
+
const newSampleIds = newSamples.map(d => d.id);
|
|
1042
|
+
|
|
1043
|
+
// Save only NEW samples to S3 body (incremental)
|
|
1044
|
+
if (newSamples.length > 0) {
|
|
1045
|
+
await storage.set(
|
|
1046
|
+
storage.getPluginKey(resourceName, 'training', 'data', modelName, `v${version}`),
|
|
1047
|
+
{
|
|
1048
|
+
modelName,
|
|
1049
|
+
version,
|
|
1050
|
+
samples: newSamples, // Only new samples
|
|
1051
|
+
features: modelConfig.features,
|
|
1052
|
+
target: modelConfig.target,
|
|
1053
|
+
savedAt: new Date().toISOString()
|
|
1054
|
+
},
|
|
1055
|
+
{ behavior: 'body-only' } // Dataset goes to S3 body
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
998
1058
|
|
|
999
|
-
//
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1059
|
+
// Append metadata to history (no full dataset duplication)
|
|
1060
|
+
const historyEntry = {
|
|
1061
|
+
version,
|
|
1062
|
+
totalSamples: processedData.length, // Total cumulative
|
|
1063
|
+
newSamples: newSamples.length, // Only new in this version
|
|
1064
|
+
sampleIds: Array.from(currentSampleIds), // All IDs for this version
|
|
1065
|
+
newSampleIds, // IDs of new samples
|
|
1066
|
+
storageKey: newSamples.length > 0 ? `training/data/${modelName}/v${version}` : null,
|
|
1067
|
+
metrics: {
|
|
1068
|
+
loss: modelStats.loss,
|
|
1069
|
+
accuracy: modelStats.accuracy,
|
|
1070
|
+
r2: modelStats.r2
|
|
1071
|
+
},
|
|
1072
|
+
trainedAt: new Date().toISOString()
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
history.push(historyEntry);
|
|
1076
|
+
|
|
1077
|
+
// Save updated history (metadata only, no full datasets)
|
|
1078
|
+
await storage.set(
|
|
1079
|
+
storage.getPluginKey(resourceName, 'training', 'history', modelName),
|
|
1080
|
+
{
|
|
1081
|
+
modelName,
|
|
1082
|
+
type: 'training_history',
|
|
1083
|
+
totalTrainings: history.length,
|
|
1084
|
+
latestVersion: version,
|
|
1085
|
+
history, // Array of metadata entries (not full data)
|
|
1086
|
+
updatedAt: new Date().toISOString()
|
|
1087
|
+
},
|
|
1088
|
+
{ behavior: 'body-overflow' } // History metadata
|
|
1089
|
+
);
|
|
1008
1090
|
|
|
1009
1091
|
if (this.config.verbose) {
|
|
1010
|
-
console.log(`[MLPlugin]
|
|
1092
|
+
console.log(`[MLPlugin] Saved training data for "${modelName}" v${version}: ${newSamples.length} new samples (total: ${processedData.length}, storage: resource=${resourceName}/plugin=ml/training/data/${modelName}/v${version})`);
|
|
1011
1093
|
}
|
|
1012
1094
|
} else {
|
|
1013
1095
|
// Legacy: Replace training data (non-incremental)
|
|
1014
|
-
await storage.
|
|
1015
|
-
modelName,
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1096
|
+
await storage.set(
|
|
1097
|
+
storage.getPluginKey(resourceName, 'training', 'data', modelName, 'latest'),
|
|
1098
|
+
{
|
|
1099
|
+
modelName,
|
|
1100
|
+
type: 'training_data',
|
|
1101
|
+
samples: processedData,
|
|
1102
|
+
features: modelConfig.features,
|
|
1103
|
+
target: modelConfig.target,
|
|
1104
|
+
savedAt: new Date().toISOString()
|
|
1105
|
+
},
|
|
1106
|
+
{ behavior: 'body-only' }
|
|
1107
|
+
);
|
|
1023
1108
|
|
|
1024
1109
|
if (this.config.verbose) {
|
|
1025
|
-
console.log(`[MLPlugin] Saved training data for "${modelName}" (${
|
|
1110
|
+
console.log(`[MLPlugin] Saved training data for "${modelName}" (${processedData.length} samples) to S3 (resource=${resourceName}/plugin=ml/training/data/${modelName}/latest)`);
|
|
1026
1111
|
}
|
|
1027
1112
|
}
|
|
1028
1113
|
} catch (error) {
|
|
@@ -1037,23 +1122,28 @@ export class MLPlugin extends Plugin {
|
|
|
1037
1122
|
async _loadModel(modelName) {
|
|
1038
1123
|
try {
|
|
1039
1124
|
const storage = this.getStorage();
|
|
1125
|
+
const modelConfig = this.config.models[modelName];
|
|
1126
|
+
const resourceName = modelConfig.resource;
|
|
1040
1127
|
const enableVersioning = this.config.enableVersioning;
|
|
1041
1128
|
|
|
1042
1129
|
if (enableVersioning) {
|
|
1043
|
-
// Load active version
|
|
1044
|
-
const [okRef, errRef, activeRef] = await tryFn(() =>
|
|
1130
|
+
// Load active version reference
|
|
1131
|
+
const [okRef, errRef, activeRef] = await tryFn(() =>
|
|
1132
|
+
storage.get(storage.getPluginKey(resourceName, 'metadata', modelName, 'active'))
|
|
1133
|
+
);
|
|
1045
1134
|
|
|
1046
1135
|
if (okRef && activeRef && activeRef.version) {
|
|
1047
1136
|
// Load the active version
|
|
1048
1137
|
const version = activeRef.version;
|
|
1049
|
-
const [ok, err, versionData] = await tryFn(() =>
|
|
1138
|
+
const [ok, err, versionData] = await tryFn(() =>
|
|
1139
|
+
storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version}`))
|
|
1140
|
+
);
|
|
1050
1141
|
|
|
1051
|
-
if (ok && versionData) {
|
|
1052
|
-
|
|
1053
|
-
await this.models[modelName].import(modelData);
|
|
1142
|
+
if (ok && versionData && versionData.modelData) {
|
|
1143
|
+
await this.models[modelName].import(versionData.modelData);
|
|
1054
1144
|
|
|
1055
1145
|
if (this.config.verbose) {
|
|
1056
|
-
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (active) from
|
|
1146
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (active) from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
|
|
1057
1147
|
}
|
|
1058
1148
|
return;
|
|
1059
1149
|
}
|
|
@@ -1063,14 +1153,15 @@ export class MLPlugin extends Plugin {
|
|
|
1063
1153
|
const versionInfo = this.modelVersions.get(modelName);
|
|
1064
1154
|
if (versionInfo && versionInfo.latestVersion > 0) {
|
|
1065
1155
|
const version = versionInfo.latestVersion;
|
|
1066
|
-
const [ok, err, versionData] = await tryFn(() =>
|
|
1156
|
+
const [ok, err, versionData] = await tryFn(() =>
|
|
1157
|
+
storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version}`))
|
|
1158
|
+
);
|
|
1067
1159
|
|
|
1068
|
-
if (ok && versionData) {
|
|
1069
|
-
|
|
1070
|
-
await this.models[modelName].import(modelData);
|
|
1160
|
+
if (ok && versionData && versionData.modelData) {
|
|
1161
|
+
await this.models[modelName].import(versionData.modelData);
|
|
1071
1162
|
|
|
1072
1163
|
if (this.config.verbose) {
|
|
1073
|
-
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (latest) from
|
|
1164
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (latest) from S3`);
|
|
1074
1165
|
}
|
|
1075
1166
|
return;
|
|
1076
1167
|
}
|
|
@@ -1081,20 +1172,21 @@ export class MLPlugin extends Plugin {
|
|
|
1081
1172
|
}
|
|
1082
1173
|
} else {
|
|
1083
1174
|
// Legacy: Load non-versioned model
|
|
1084
|
-
const [ok, err, record] = await tryFn(() =>
|
|
1175
|
+
const [ok, err, record] = await tryFn(() =>
|
|
1176
|
+
storage.get(storage.getPluginKey(resourceName, 'models', modelName, 'latest'))
|
|
1177
|
+
);
|
|
1085
1178
|
|
|
1086
|
-
if (!ok || !record) {
|
|
1179
|
+
if (!ok || !record || !record.modelData) {
|
|
1087
1180
|
if (this.config.verbose) {
|
|
1088
1181
|
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
1089
1182
|
}
|
|
1090
1183
|
return;
|
|
1091
1184
|
}
|
|
1092
1185
|
|
|
1093
|
-
|
|
1094
|
-
await this.models[modelName].import(modelData);
|
|
1186
|
+
await this.models[modelName].import(record.modelData);
|
|
1095
1187
|
|
|
1096
1188
|
if (this.config.verbose) {
|
|
1097
|
-
console.log(`[MLPlugin] Loaded model "${modelName}" from
|
|
1189
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
|
|
1098
1190
|
}
|
|
1099
1191
|
}
|
|
1100
1192
|
} catch (error) {
|
|
@@ -1103,29 +1195,82 @@ export class MLPlugin extends Plugin {
|
|
|
1103
1195
|
}
|
|
1104
1196
|
|
|
1105
1197
|
/**
|
|
1106
|
-
* Load training data from plugin storage
|
|
1198
|
+
* Load training data from plugin storage (reconstructs specific version from incremental data)
|
|
1107
1199
|
* @param {string} modelName - Model name
|
|
1200
|
+
* @param {number} version - Version number (optional, defaults to latest)
|
|
1108
1201
|
* @returns {Object|null} Training data or null if not found
|
|
1109
1202
|
*/
|
|
1110
|
-
async getTrainingData(modelName) {
|
|
1203
|
+
async getTrainingData(modelName, version = null) {
|
|
1111
1204
|
try {
|
|
1112
1205
|
const storage = this.getStorage();
|
|
1113
|
-
const
|
|
1206
|
+
const modelConfig = this.config.models[modelName];
|
|
1207
|
+
const resourceName = modelConfig.resource;
|
|
1208
|
+
const enableVersioning = this.config.enableVersioning;
|
|
1209
|
+
|
|
1210
|
+
if (!enableVersioning) {
|
|
1211
|
+
// Legacy: Load non-versioned training data
|
|
1212
|
+
const [ok, err, record] = await tryFn(() =>
|
|
1213
|
+
storage.get(storage.getPluginKey(resourceName, 'training', 'data', modelName, 'latest'))
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
if (!ok || !record) {
|
|
1217
|
+
if (this.config.verbose) {
|
|
1218
|
+
console.log(`[MLPlugin] No saved training data found for "${modelName}"`);
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1114
1222
|
|
|
1115
|
-
|
|
1223
|
+
return {
|
|
1224
|
+
modelName: record.modelName,
|
|
1225
|
+
samples: record.samples,
|
|
1226
|
+
features: record.features,
|
|
1227
|
+
target: record.target,
|
|
1228
|
+
data: record.samples,
|
|
1229
|
+
savedAt: record.savedAt
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Versioned: Reconstruct dataset from incremental versions
|
|
1234
|
+
const [okHistory, errHistory, historyData] = await tryFn(() =>
|
|
1235
|
+
storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName))
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
if (!okHistory || !historyData || !historyData.history) {
|
|
1116
1239
|
if (this.config.verbose) {
|
|
1117
|
-
console.log(`[MLPlugin] No
|
|
1240
|
+
console.log(`[MLPlugin] No training history found for "${modelName}"`);
|
|
1118
1241
|
}
|
|
1119
1242
|
return null;
|
|
1120
1243
|
}
|
|
1121
1244
|
|
|
1245
|
+
const targetVersion = version || historyData.latestVersion;
|
|
1246
|
+
const reconstructedSamples = [];
|
|
1247
|
+
|
|
1248
|
+
// Load and combine all versions up to target version
|
|
1249
|
+
for (const entry of historyData.history) {
|
|
1250
|
+
if (entry.version > targetVersion) break;
|
|
1251
|
+
|
|
1252
|
+
if (entry.storageKey && entry.newSamples > 0) {
|
|
1253
|
+
const [ok, err, versionData] = await tryFn(() =>
|
|
1254
|
+
storage.get(storage.getPluginKey(resourceName, 'training', 'data', modelName, `v${entry.version}`))
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
if (ok && versionData && versionData.samples) {
|
|
1258
|
+
reconstructedSamples.push(...versionData.samples);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const targetEntry = historyData.history.find(e => e.version === targetVersion);
|
|
1264
|
+
|
|
1122
1265
|
return {
|
|
1123
|
-
modelName
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1266
|
+
modelName,
|
|
1267
|
+
version: targetVersion,
|
|
1268
|
+
samples: reconstructedSamples,
|
|
1269
|
+
totalSamples: reconstructedSamples.length,
|
|
1270
|
+
features: modelConfig.features,
|
|
1271
|
+
target: modelConfig.target,
|
|
1272
|
+
metrics: targetEntry?.metrics,
|
|
1273
|
+
savedAt: targetEntry?.trainedAt
|
|
1129
1274
|
};
|
|
1130
1275
|
} catch (error) {
|
|
1131
1276
|
console.error(`[MLPlugin] Failed to load training data for "${modelName}":`, error.message);
|
|
@@ -1134,16 +1279,35 @@ export class MLPlugin extends Plugin {
|
|
|
1134
1279
|
}
|
|
1135
1280
|
|
|
1136
1281
|
/**
|
|
1137
|
-
* Delete model from plugin storage
|
|
1282
|
+
* Delete model from plugin storage (all versions)
|
|
1138
1283
|
* @private
|
|
1139
1284
|
*/
|
|
1140
1285
|
async _deleteModel(modelName) {
|
|
1141
1286
|
try {
|
|
1142
1287
|
const storage = this.getStorage();
|
|
1143
|
-
|
|
1288
|
+
const modelConfig = this.config.models[modelName];
|
|
1289
|
+
const resourceName = modelConfig.resource;
|
|
1290
|
+
const enableVersioning = this.config.enableVersioning;
|
|
1291
|
+
|
|
1292
|
+
if (enableVersioning) {
|
|
1293
|
+
// Delete all versions
|
|
1294
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
1295
|
+
if (versionInfo && versionInfo.latestVersion > 0) {
|
|
1296
|
+
for (let v = 1; v <= versionInfo.latestVersion; v++) {
|
|
1297
|
+
await storage.delete(storage.getPluginKey(resourceName, 'models', modelName, `v${v}`));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Delete metadata
|
|
1302
|
+
await storage.delete(storage.getPluginKey(resourceName, 'metadata', modelName, 'active'));
|
|
1303
|
+
await storage.delete(storage.getPluginKey(resourceName, 'metadata', modelName, 'versions'));
|
|
1304
|
+
} else {
|
|
1305
|
+
// Delete non-versioned model
|
|
1306
|
+
await storage.delete(storage.getPluginKey(resourceName, 'models', modelName, 'latest'));
|
|
1307
|
+
}
|
|
1144
1308
|
|
|
1145
1309
|
if (this.config.verbose) {
|
|
1146
|
-
console.log(`[MLPlugin] Deleted model "${modelName}" from plugin
|
|
1310
|
+
console.log(`[MLPlugin] Deleted model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/)`);
|
|
1147
1311
|
}
|
|
1148
1312
|
} catch (error) {
|
|
1149
1313
|
// Ignore errors (model might not exist)
|
|
@@ -1154,16 +1318,39 @@ export class MLPlugin extends Plugin {
|
|
|
1154
1318
|
}
|
|
1155
1319
|
|
|
1156
1320
|
/**
|
|
1157
|
-
* Delete training data from plugin storage
|
|
1321
|
+
* Delete training data from plugin storage (all versions)
|
|
1158
1322
|
* @private
|
|
1159
1323
|
*/
|
|
1160
1324
|
async _deleteTrainingData(modelName) {
|
|
1161
1325
|
try {
|
|
1162
1326
|
const storage = this.getStorage();
|
|
1163
|
-
|
|
1327
|
+
const modelConfig = this.config.models[modelName];
|
|
1328
|
+
const resourceName = modelConfig.resource;
|
|
1329
|
+
const enableVersioning = this.config.enableVersioning;
|
|
1330
|
+
|
|
1331
|
+
if (enableVersioning) {
|
|
1332
|
+
// Delete all version data
|
|
1333
|
+
const [ok, err, historyData] = await tryFn(() =>
|
|
1334
|
+
storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName))
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
if (ok && historyData && historyData.history) {
|
|
1338
|
+
for (const entry of historyData.history) {
|
|
1339
|
+
if (entry.storageKey) {
|
|
1340
|
+
await storage.delete(storage.getPluginKey(resourceName, 'training', 'data', modelName, `v${entry.version}`));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Delete history
|
|
1346
|
+
await storage.delete(storage.getPluginKey(resourceName, 'training', 'history', modelName));
|
|
1347
|
+
} else {
|
|
1348
|
+
// Delete non-versioned training data
|
|
1349
|
+
await storage.delete(storage.getPluginKey(resourceName, 'training', 'data', modelName, 'latest'));
|
|
1350
|
+
}
|
|
1164
1351
|
|
|
1165
1352
|
if (this.config.verbose) {
|
|
1166
|
-
console.log(`[MLPlugin] Deleted training data for "${modelName}" from plugin
|
|
1353
|
+
console.log(`[MLPlugin] Deleted training data for "${modelName}" from S3 (resource=${resourceName}/plugin=ml/training/)`);
|
|
1167
1354
|
}
|
|
1168
1355
|
} catch (error) {
|
|
1169
1356
|
// Ignore errors (training data might not exist)
|
|
@@ -1185,20 +1372,21 @@ export class MLPlugin extends Plugin {
|
|
|
1185
1372
|
|
|
1186
1373
|
try {
|
|
1187
1374
|
const storage = this.getStorage();
|
|
1375
|
+
const modelConfig = this.config.models[modelName];
|
|
1376
|
+
const resourceName = modelConfig.resource;
|
|
1188
1377
|
const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
|
|
1189
1378
|
const versions = [];
|
|
1190
1379
|
|
|
1191
1380
|
// Load each version
|
|
1192
1381
|
for (let v = 1; v <= versionInfo.latestVersion; v++) {
|
|
1193
|
-
const [ok, err, versionData] = await tryFn(() => storage.get(`
|
|
1382
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${v}`)));
|
|
1194
1383
|
|
|
1195
1384
|
if (ok && versionData) {
|
|
1196
|
-
const metrics = versionData.metrics ? JSON.parse(versionData.metrics) : {};
|
|
1197
1385
|
versions.push({
|
|
1198
1386
|
version: v,
|
|
1199
1387
|
savedAt: versionData.savedAt,
|
|
1200
1388
|
isCurrent: v === versionInfo.currentVersion,
|
|
1201
|
-
metrics
|
|
1389
|
+
metrics: versionData.metrics
|
|
1202
1390
|
});
|
|
1203
1391
|
}
|
|
1204
1392
|
}
|
|
@@ -1226,14 +1414,19 @@ export class MLPlugin extends Plugin {
|
|
|
1226
1414
|
|
|
1227
1415
|
try {
|
|
1228
1416
|
const storage = this.getStorage();
|
|
1229
|
-
const
|
|
1417
|
+
const modelConfig = this.config.models[modelName];
|
|
1418
|
+
const resourceName = modelConfig.resource;
|
|
1419
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version}`)));
|
|
1230
1420
|
|
|
1231
1421
|
if (!ok || !versionData) {
|
|
1232
1422
|
throw new MLError(`Version ${version} not found for model "${modelName}"`, { modelName, version });
|
|
1233
1423
|
}
|
|
1234
1424
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1425
|
+
if (!versionData.modelData) {
|
|
1426
|
+
throw new MLError(`Model data not found in version ${version}`, { modelName, version });
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
await this.models[modelName].import(versionData.modelData);
|
|
1237
1430
|
|
|
1238
1431
|
// Update current version in memory (don't save to storage yet)
|
|
1239
1432
|
const versionInfo = this.modelVersions.get(modelName);
|
|
@@ -1267,6 +1460,9 @@ export class MLPlugin extends Plugin {
|
|
|
1267
1460
|
throw new MLError('Versioning is not enabled', { modelName });
|
|
1268
1461
|
}
|
|
1269
1462
|
|
|
1463
|
+
const modelConfig = this.config.models[modelName];
|
|
1464
|
+
const resourceName = modelConfig.resource;
|
|
1465
|
+
|
|
1270
1466
|
// Load the version into the model
|
|
1271
1467
|
await this.loadModelVersion(modelName, version);
|
|
1272
1468
|
|
|
@@ -1275,7 +1471,7 @@ export class MLPlugin extends Plugin {
|
|
|
1275
1471
|
|
|
1276
1472
|
// Update active reference
|
|
1277
1473
|
const storage = this.getStorage();
|
|
1278
|
-
await storage.
|
|
1474
|
+
await storage.set(storage.getPluginKey(resourceName, 'metadata', modelName, 'active'), {
|
|
1279
1475
|
modelName,
|
|
1280
1476
|
version,
|
|
1281
1477
|
type: 'reference',
|
|
@@ -1302,7 +1498,9 @@ export class MLPlugin extends Plugin {
|
|
|
1302
1498
|
|
|
1303
1499
|
try {
|
|
1304
1500
|
const storage = this.getStorage();
|
|
1305
|
-
const
|
|
1501
|
+
const modelConfig = this.config.models[modelName];
|
|
1502
|
+
const resourceName = modelConfig.resource;
|
|
1503
|
+
const [ok, err, historyData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName)));
|
|
1306
1504
|
|
|
1307
1505
|
if (!ok || !historyData) {
|
|
1308
1506
|
return null;
|
|
@@ -1335,9 +1533,11 @@ export class MLPlugin extends Plugin {
|
|
|
1335
1533
|
|
|
1336
1534
|
try {
|
|
1337
1535
|
const storage = this.getStorage();
|
|
1536
|
+
const modelConfig = this.config.models[modelName];
|
|
1537
|
+
const resourceName = modelConfig.resource;
|
|
1338
1538
|
|
|
1339
|
-
const [ok1, err1, v1Data] = await tryFn(() => storage.get(`
|
|
1340
|
-
const [ok2, err2, v2Data] = await tryFn(() => storage.get(`
|
|
1539
|
+
const [ok1, err1, v1Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version1}`)));
|
|
1540
|
+
const [ok2, err2, v2Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version2}`)));
|
|
1341
1541
|
|
|
1342
1542
|
if (!ok1 || !v1Data) {
|
|
1343
1543
|
throw new MLError(`Version ${version1} not found`, { modelName, version: version1 });
|