Senum2001 commited on
Commit
01d0daa
·
1 Parent(s): 30b81fd

Add model versioning and training history tracking system

Browse files
app.py CHANGED
@@ -6,6 +6,7 @@ Integrated with feedback learning pipeline for continuous model improvement
6
  from flask import Flask, request, jsonify
7
  from inference_core import run_pipeline_for_image, download_image_from_url, upload_to_cloudinary, model, device
8
  from scripts.feedback_learning_pipeline import initialize_feedback_pipeline, run_feedback_training
 
9
  import os
10
 
11
  app = Flask(__name__)
@@ -13,6 +14,9 @@ app = Flask(__name__)
13
  # Initialize feedback learning pipeline
14
  feedback_pipeline = initialize_feedback_pipeline(model, device)
15
 
 
 
 
16
 
17
  @app.route("/", methods=["GET"])
18
  def home():
@@ -24,7 +28,11 @@ def home():
24
  "/health": "GET - Health check",
25
  "/infer": "POST - Run inference on image URL",
26
  "/feedback/stats": "GET - Get feedback statistics and training status",
27
- "/feedback/train": "POST - Manually trigger feedback training cycle"
 
 
 
 
28
  },
29
  "example_request": {
30
  "method": "POST",
@@ -37,6 +45,11 @@ def home():
37
  "description": "User corrections are automatically fetched from Supabase",
38
  "training_trigger": "Automatic when 10+ new feedback samples available",
39
  "manual_training": "POST /feedback/train to trigger immediately"
 
 
 
 
 
40
  }
41
  })
42
 
@@ -72,6 +85,74 @@ def trigger_training():
72
  return jsonify({"error": str(e)}), 500
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  @app.route("/infer", methods=["POST"])
76
  def infer():
77
  """
 
6
  from flask import Flask, request, jsonify
7
  from inference_core import run_pipeline_for_image, download_image_from_url, upload_to_cloudinary, model, device
8
  from scripts.feedback_learning_pipeline import initialize_feedback_pipeline, run_feedback_training
9
+ from scripts.model_versioning import initialize_model_tracker
10
  import os
11
 
12
  app = Flask(__name__)
 
14
  # Initialize feedback learning pipeline
15
  feedback_pipeline = initialize_feedback_pipeline(model, device)
16
 
17
+ # Initialize model versioning tracker
18
+ model_tracker = initialize_model_tracker()
19
+
20
 
21
  @app.route("/", methods=["GET"])
22
  def home():
 
28
  "/health": "GET - Health check",
29
  "/infer": "POST - Run inference on image URL",
30
  "/feedback/stats": "GET - Get feedback statistics and training status",
31
+ "/feedback/train": "POST - Manually trigger feedback training cycle",
32
+ "/model/current": "GET - Get current model version and parameters",
33
+ "/model/versions": "GET - Get model version history",
34
+ "/model/training-history": "GET - Get training cycle history",
35
+ "/model/compare": "POST - Compare model versions"
36
  },
37
  "example_request": {
38
  "method": "POST",
 
45
  "description": "User corrections are automatically fetched from Supabase",
46
  "training_trigger": "Automatic when 10+ new feedback samples available",
47
  "manual_training": "POST /feedback/train to trigger immediately"
48
+ },
49
+ "versioning_info": {
50
+ "description": "Model versions and training history tracked automatically",
51
+ "view_current": "GET /model/current to see active model parameters",
52
+ "view_history": "GET /model/versions to see all versions"
53
  }
54
  })
55
 
 
85
  return jsonify({"error": str(e)}), 500
86
 
87
 
88
+ @app.route("/model/current", methods=["GET"])
89
+ def get_current_model():
90
+ """
91
+ Get current active model version and parameters
92
+ """
93
+ try:
94
+ current_state = model_tracker.get_current_model_state()
95
+ return jsonify(current_state), 200
96
+ except Exception as e:
97
+ return jsonify({"error": str(e)}), 500
98
+
99
+
100
+ @app.route("/model/versions", methods=["GET"])
101
+ def get_model_versions():
102
+ """
103
+ Get model version history
104
+ Query params: limit (default: 20)
105
+ """
106
+ try:
107
+ limit = int(request.args.get('limit', 20))
108
+ versions = model_tracker.get_version_history(limit=limit)
109
+ return jsonify({
110
+ "total": len(versions),
111
+ "versions": versions
112
+ }), 200
113
+ except Exception as e:
114
+ return jsonify({"error": str(e)}), 500
115
+
116
+
117
+ @app.route("/model/training-history", methods=["GET"])
118
+ def get_training_history():
119
+ """
120
+ Get training cycle history
121
+ Query params: limit (default: 20)
122
+ """
123
+ try:
124
+ limit = int(request.args.get('limit', 20))
125
+ history = model_tracker.get_training_history(limit=limit)
126
+ return jsonify({
127
+ "total": len(history),
128
+ "training_cycles": history
129
+ }), 200
130
+ except Exception as e:
131
+ return jsonify({"error": str(e)}), 500
132
+
133
+
134
+ @app.route("/model/compare", methods=["POST"])
135
+ def compare_versions():
136
+ """
137
+ Compare multiple model versions
138
+ Request JSON: {"version_ids": ["id1", "id2", ...]}
139
+ """
140
+ try:
141
+ data = request.get_json()
142
+ if not data or "version_ids" not in data:
143
+ return jsonify({"error": "Missing version_ids"}), 400
144
+
145
+ version_ids = data["version_ids"]
146
+ comparison = model_tracker.generate_comparison_table(version_ids)
147
+
148
+ return jsonify({
149
+ "comparison": comparison,
150
+ "version_count": len(version_ids)
151
+ }), 200
152
+ except Exception as e:
153
+ return jsonify({"error": str(e)}), 500
154
+
155
+
156
  @app.route("/infer", methods=["POST"])
157
  def infer():
158
  """
scripts/feedback_learning_pipeline.py CHANGED
@@ -13,6 +13,14 @@ from supabase import create_client, Client
13
  from PIL import Image
14
  import tempfile
15
 
 
 
 
 
 
 
 
 
16
  # Supabase configuration
17
  SUPABASE_URL = os.getenv("SUPABASE_URL", "https://xbcgrpqiibicestnhytt.supabase.co")
18
  SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhiY2dycHFpaWJpY2VzdG5oeXR0Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NTkxMzk3MywiZXhwIjoyMDcxNDg5OTczfQ.sANBuVZ6gdYc5kHkxTXZ67jtE9QHPw5HFaUKffP1Jrs")
@@ -40,6 +48,12 @@ class FeedbackLearningPipeline:
40
  self.device = device
41
  self.training_state = self._load_training_state()
42
 
 
 
 
 
 
 
43
  def _load_training_state(self) -> Dict[str, Any]:
44
  """Load training state from disk"""
45
  if os.path.exists(TRAINING_STATE_FILE):
@@ -252,6 +266,13 @@ class FeedbackLearningPipeline:
252
  """
253
  print(f"\n[Feedback Pipeline] Starting training cycle at {datetime.now()}")
254
 
 
 
 
 
 
 
 
255
  # Fetch new feedback
256
  feedback_logs = self.fetch_new_feedback(limit=1000)
257
 
@@ -290,6 +311,26 @@ class FeedbackLearningPipeline:
290
 
291
  self._save_training_state()
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  print(f"[Feedback Pipeline] Training cycle completed successfully")
294
  print(f"[Feedback Pipeline] Total feedback processed: {self.training_state['total_feedback_processed']}")
295
 
@@ -297,7 +338,10 @@ class FeedbackLearningPipeline:
297
  "status": "success",
298
  "corrections_processed": len(corrections),
299
  "patterns": patterns,
300
- "total_feedback_processed": self.training_state["total_feedback_processed"]
 
 
 
301
  }
302
 
303
  def get_feedback_stats(self) -> Dict[str, Any]:
 
13
  from PIL import Image
14
  import tempfile
15
 
16
+ # Import model versioning system
17
+ try:
18
+ from scripts.model_versioning import ModelVersionTracker
19
+ MODEL_VERSIONING_AVAILABLE = True
20
+ except ImportError:
21
+ MODEL_VERSIONING_AVAILABLE = False
22
+ print("[Warning] Model versioning not available")
23
+
24
  # Supabase configuration
25
  SUPABASE_URL = os.getenv("SUPABASE_URL", "https://xbcgrpqiibicestnhytt.supabase.co")
26
  SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhiY2dycHFpaWJpY2VzdG5oeXR0Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NTkxMzk3MywiZXhwIjoyMDcxNDg5OTczfQ.sANBuVZ6gdYc5kHkxTXZ67jtE9QHPw5HFaUKffP1Jrs")
 
48
  self.device = device
49
  self.training_state = self._load_training_state()
50
 
51
+ # Initialize model versioning tracker
52
+ if MODEL_VERSIONING_AVAILABLE:
53
+ self.version_tracker = ModelVersionTracker()
54
+ else:
55
+ self.version_tracker = None
56
+
57
  def _load_training_state(self) -> Dict[str, Any]:
58
  """Load training state from disk"""
59
  if os.path.exists(TRAINING_STATE_FILE):
 
266
  """
267
  print(f"\n[Feedback Pipeline] Starting training cycle at {datetime.now()}")
268
 
269
+ # Capture model state BEFORE training
270
+ before_state = None
271
+ if self.version_tracker:
272
+ before_state = self.version_tracker.get_current_model_state()
273
+ self.version_tracker.log_model_version(before_state)
274
+ print(f"[Model Versioning] Captured state before training: {before_state['version_id'][:8]}...")
275
+
276
  # Fetch new feedback
277
  feedback_logs = self.fetch_new_feedback(limit=1000)
278
 
 
311
 
312
  self._save_training_state()
313
 
314
+ # Capture model state AFTER training
315
+ after_state = None
316
+ training_cycle_id = None
317
+ if self.version_tracker:
318
+ after_state = self.version_tracker.get_current_model_state()
319
+ self.version_tracker.log_model_version(after_state)
320
+ print(f"[Model Versioning] Captured state after training: {after_state['version_id'][:8]}...")
321
+
322
+ # Log the training cycle with before/after comparison
323
+ training_cycle_id = self.version_tracker.log_training_cycle(
324
+ before_state=before_state,
325
+ after_state=after_state,
326
+ feedback_count=len(corrections),
327
+ patterns=patterns,
328
+ performance_metrics=None # TODO: Calculate actual metrics
329
+ )
330
+
331
+ if training_cycle_id:
332
+ print(f"[Training History] Logged training cycle: {training_cycle_id[:8]}...")
333
+
334
  print(f"[Feedback Pipeline] Training cycle completed successfully")
335
  print(f"[Feedback Pipeline] Total feedback processed: {self.training_state['total_feedback_processed']}")
336
 
 
338
  "status": "success",
339
  "corrections_processed": len(corrections),
340
  "patterns": patterns,
341
+ "total_feedback_processed": self.training_state["total_feedback_processed"],
342
+ "before_version_id": before_state["version_id"] if before_state else None,
343
+ "after_version_id": after_state["version_id"] if after_state else None,
344
+ "training_cycle_id": training_cycle_id
345
  }
346
 
347
  def get_feedback_stats(self) -> Dict[str, Any]:
scripts/model_versioning.py ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model Versioning & Training History System
3
+ Tracks model parameters, thresholds, and training evolution in Supabase
4
+ """
5
+ import os
6
+ import json
7
+ from datetime import datetime
8
+ from typing import Dict, Any, Optional, List
9
+ from supabase import create_client, Client
10
+ import uuid
11
+
12
+ # Supabase configuration
13
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "https://xbcgrpqiibicestnhytt.supabase.co")
14
+ SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
15
+
16
+ # Initialize Supabase client
17
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
18
+
19
+
20
+ class ModelVersionTracker:
21
+ """
22
+ Tracks model versions, parameters, and training history
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initialize the model version tracker"""
27
+ self.supabase = supabase
28
+
29
+ def get_current_model_state(self) -> Dict[str, Any]:
30
+ """
31
+ Get current model parameters and thresholds
32
+
33
+ Returns:
34
+ Dictionary containing current model state
35
+ """
36
+ # Read current model adjustments if they exist
37
+ adjustments = {}
38
+ if os.path.exists("model_adjustments.json"):
39
+ with open("model_adjustments.json", "r") as f:
40
+ adjustments = json.load(f)
41
+
42
+ # Read training state
43
+ training_state = {}
44
+ if os.path.exists("feedback_training_state.json"):
45
+ with open("feedback_training_state.json", "r") as f:
46
+ training_state = json.load(f)
47
+
48
+ # Define current model parameters
49
+ model_state = {
50
+ "version_id": str(uuid.uuid4()),
51
+ "timestamp": datetime.now().isoformat(),
52
+
53
+ # Model Configuration
54
+ "model_architecture": "PatchCore",
55
+ "backbone": "Wide ResNet-50",
56
+ "layers": ["layer2", "layer3"],
57
+ "input_size": [256, 256],
58
+
59
+ # Detection Thresholds
60
+ "anomaly_threshold": 128, # Binary mask threshold
61
+ "confidence_range": [0.3, 0.99],
62
+ "min_detection_size": 100, # Minimum pixels for detection
63
+
64
+ # Classification Thresholds
65
+ "red_color_threshold": {
66
+ "hue_range": [0, 10, 170, 180],
67
+ "saturation_min": 100,
68
+ "value_min": 100
69
+ },
70
+ "yellow_color_threshold": {
71
+ "hue_range": [20, 30],
72
+ "saturation_min": 100,
73
+ "value_min": 100
74
+ },
75
+ "orange_color_threshold": {
76
+ "hue_range": [10, 20],
77
+ "saturation_min": 100,
78
+ "value_min": 100
79
+ },
80
+
81
+ # Post-processing Parameters
82
+ "merge_distance_threshold": 20,
83
+ "iou_threshold": 0.4,
84
+ "min_contour_area": 100,
85
+
86
+ # Learned Adjustments (from feedback)
87
+ "false_positive_rate": adjustments.get("fp_rate", 0.0),
88
+ "false_negative_rate": adjustments.get("fn_rate", 0.0),
89
+ "threshold_recommendation": adjustments.get("recommendation", "Not yet calculated"),
90
+
91
+ # Training Metadata
92
+ "total_feedback_processed": training_state.get("total_feedback_processed", 0),
93
+ "last_training_time": training_state.get("last_training_time"),
94
+ "training_runs_count": len(training_state.get("training_runs", []))
95
+ }
96
+
97
+ return model_state
98
+
99
+ def log_model_version(self, model_state: Dict[str, Any]) -> Optional[str]:
100
+ """
101
+ Log current model version to Supabase
102
+
103
+ Args:
104
+ model_state: Dictionary containing model parameters
105
+
106
+ Returns:
107
+ Version ID if successful, None otherwise
108
+ """
109
+ try:
110
+ # Prepare record for database
111
+ record = {
112
+ "version_id": model_state["version_id"],
113
+ "timestamp": model_state["timestamp"],
114
+ "model_architecture": model_state["model_architecture"],
115
+ "backbone": model_state["backbone"],
116
+ "parameters": {
117
+ "layers": model_state["layers"],
118
+ "input_size": model_state["input_size"],
119
+ "anomaly_threshold": model_state["anomaly_threshold"],
120
+ "confidence_range": model_state["confidence_range"],
121
+ "min_detection_size": model_state["min_detection_size"]
122
+ },
123
+ "thresholds": {
124
+ "red_color": model_state["red_color_threshold"],
125
+ "yellow_color": model_state["yellow_color_threshold"],
126
+ "orange_color": model_state["orange_color_threshold"],
127
+ "merge_distance": model_state["merge_distance_threshold"],
128
+ "iou": model_state["iou_threshold"],
129
+ "min_contour_area": model_state["min_contour_area"]
130
+ },
131
+ "learned_adjustments": {
132
+ "false_positive_rate": model_state["false_positive_rate"],
133
+ "false_negative_rate": model_state["false_negative_rate"],
134
+ "recommendation": model_state["threshold_recommendation"]
135
+ },
136
+ "training_metadata": {
137
+ "total_feedback_processed": model_state["total_feedback_processed"],
138
+ "last_training_time": model_state["last_training_time"],
139
+ "training_runs_count": model_state["training_runs_count"]
140
+ },
141
+ "is_active": True
142
+ }
143
+
144
+ # Insert into database
145
+ response = self.supabase.table('model_versions').insert(record).execute()
146
+
147
+ if response.data:
148
+ print(f"[Model Versioning] Logged version {model_state['version_id']}")
149
+ return model_state["version_id"]
150
+ else:
151
+ print("[Model Versioning] Failed to log version")
152
+ return None
153
+
154
+ except Exception as e:
155
+ print(f"[Model Versioning] Error logging version: {e}")
156
+ return None
157
+
158
+ def log_training_cycle(self,
159
+ before_state: Dict[str, Any],
160
+ after_state: Dict[str, Any],
161
+ feedback_count: int,
162
+ patterns: Dict[str, Any],
163
+ performance_metrics: Optional[Dict[str, Any]] = None) -> Optional[str]:
164
+ """
165
+ Log a training cycle with before/after comparison
166
+
167
+ Args:
168
+ before_state: Model state before training
169
+ after_state: Model state after training
170
+ feedback_count: Number of feedback samples processed
171
+ patterns: Pattern analysis from feedback
172
+ performance_metrics: Optional performance metrics
173
+
174
+ Returns:
175
+ Training cycle ID if successful
176
+ """
177
+ try:
178
+ cycle_id = str(uuid.uuid4())
179
+
180
+ # Calculate parameter changes
181
+ parameter_changes = self._calculate_parameter_changes(before_state, after_state)
182
+
183
+ record = {
184
+ "cycle_id": cycle_id,
185
+ "timestamp": datetime.now().isoformat(),
186
+ "before_version_id": before_state["version_id"],
187
+ "after_version_id": after_state["version_id"],
188
+ "feedback_samples_processed": feedback_count,
189
+
190
+ # Pattern Analysis
191
+ "feedback_patterns": {
192
+ "label_changes": patterns.get("label_changes", []),
193
+ "bbox_adjustments": patterns.get("bbox_adjustments", []),
194
+ "false_positives": patterns.get("false_positives", 0),
195
+ "false_negatives": patterns.get("false_negatives", 0)
196
+ },
197
+
198
+ # Parameter Changes
199
+ "parameter_changes": parameter_changes,
200
+
201
+ # Performance Metrics (if available)
202
+ "performance_metrics": performance_metrics or {
203
+ "accuracy_improvement": "Not yet calculated",
204
+ "precision_improvement": "Not yet calculated",
205
+ "recall_improvement": "Not yet calculated"
206
+ },
207
+
208
+ # Recommendations
209
+ "threshold_recommendation": after_state.get("threshold_recommendation", ""),
210
+
211
+ # Status
212
+ "status": "completed",
213
+ "notes": f"Processed {feedback_count} feedback samples"
214
+ }
215
+
216
+ # Insert into database
217
+ response = self.supabase.table('training_history').insert(record).execute()
218
+
219
+ if response.data:
220
+ print(f"[Training History] Logged cycle {cycle_id}")
221
+ return cycle_id
222
+ else:
223
+ print("[Training History] Failed to log cycle")
224
+ return None
225
+
226
+ except Exception as e:
227
+ print(f"[Training History] Error logging cycle: {e}")
228
+ return None
229
+
230
+ def _calculate_parameter_changes(self, before: Dict[str, Any], after: Dict[str, Any]) -> Dict[str, Any]:
231
+ """Calculate what changed between before and after states"""
232
+ changes = {}
233
+
234
+ # Compare false positive/negative rates
235
+ if before["false_positive_rate"] != after["false_positive_rate"]:
236
+ changes["false_positive_rate"] = {
237
+ "before": before["false_positive_rate"],
238
+ "after": after["false_positive_rate"],
239
+ "delta": after["false_positive_rate"] - before["false_positive_rate"]
240
+ }
241
+
242
+ if before["false_negative_rate"] != after["false_negative_rate"]:
243
+ changes["false_negative_rate"] = {
244
+ "before": before["false_negative_rate"],
245
+ "after": after["false_negative_rate"],
246
+ "delta": after["false_negative_rate"] - before["false_negative_rate"]
247
+ }
248
+
249
+ # Compare training metadata
250
+ if before["total_feedback_processed"] != after["total_feedback_processed"]:
251
+ changes["total_feedback_processed"] = {
252
+ "before": before["total_feedback_processed"],
253
+ "after": after["total_feedback_processed"],
254
+ "delta": after["total_feedback_processed"] - before["total_feedback_processed"]
255
+ }
256
+
257
+ if before["threshold_recommendation"] != after["threshold_recommendation"]:
258
+ changes["threshold_recommendation"] = {
259
+ "before": before["threshold_recommendation"],
260
+ "after": after["threshold_recommendation"]
261
+ }
262
+
263
+ return changes
264
+
265
+ def get_version_history(self, limit: int = 20) -> List[Dict[str, Any]]:
266
+ """
267
+ Get recent model version history
268
+
269
+ Args:
270
+ limit: Maximum number of versions to retrieve
271
+
272
+ Returns:
273
+ List of model versions
274
+ """
275
+ try:
276
+ response = self.supabase.table('model_versions')\
277
+ .select('*')\
278
+ .order('timestamp', desc=True)\
279
+ .limit(limit)\
280
+ .execute()
281
+
282
+ return response.data if response.data else []
283
+
284
+ except Exception as e:
285
+ print(f"[Model Versioning] Error fetching history: {e}")
286
+ return []
287
+
288
+ def get_training_history(self, limit: int = 20) -> List[Dict[str, Any]]:
289
+ """
290
+ Get recent training cycles
291
+
292
+ Args:
293
+ limit: Maximum number of cycles to retrieve
294
+
295
+ Returns:
296
+ List of training cycles
297
+ """
298
+ try:
299
+ response = self.supabase.table('training_history')\
300
+ .select('*')\
301
+ .order('timestamp', desc=True)\
302
+ .limit(limit)\
303
+ .execute()
304
+
305
+ return response.data if response.data else []
306
+
307
+ except Exception as e:
308
+ print(f"[Training History] Error fetching history: {e}")
309
+ return []
310
+
311
+ def get_active_version(self) -> Optional[Dict[str, Any]]:
312
+ """
313
+ Get currently active model version
314
+
315
+ Returns:
316
+ Active model version or None
317
+ """
318
+ try:
319
+ response = self.supabase.table('model_versions')\
320
+ .select('*')\
321
+ .eq('is_active', True)\
322
+ .order('timestamp', desc=True)\
323
+ .limit(1)\
324
+ .execute()
325
+
326
+ if response.data:
327
+ return response.data[0]
328
+ return None
329
+
330
+ except Exception as e:
331
+ print(f"[Model Versioning] Error fetching active version: {e}")
332
+ return None
333
+
334
+ def generate_comparison_table(self, version_ids: List[str]) -> str:
335
+ """
336
+ Generate a comparison table between model versions
337
+
338
+ Args:
339
+ version_ids: List of version IDs to compare
340
+
341
+ Returns:
342
+ Formatted comparison table string
343
+ """
344
+ try:
345
+ versions = []
346
+ for vid in version_ids:
347
+ response = self.supabase.table('model_versions')\
348
+ .select('*')\
349
+ .eq('version_id', vid)\
350
+ .execute()
351
+ if response.data:
352
+ versions.append(response.data[0])
353
+
354
+ if not versions:
355
+ return "No versions found"
356
+
357
+ # Generate comparison table
358
+ table = "\n" + "=" * 100 + "\n"
359
+ table += "MODEL VERSION COMPARISON\n"
360
+ table += "=" * 100 + "\n\n"
361
+
362
+ for i, v in enumerate(versions):
363
+ table += f"Version {i+1}: {v['version_id'][:8]}...\n"
364
+ table += f"Timestamp: {v['timestamp']}\n"
365
+ table += f"Architecture: {v['model_architecture']} ({v['backbone']})\n"
366
+ table += f"False Positive Rate: {v['learned_adjustments']['false_positive_rate']:.2%}\n"
367
+ table += f"False Negative Rate: {v['learned_adjustments']['false_negative_rate']:.2%}\n"
368
+ table += f"Feedback Processed: {v['training_metadata']['total_feedback_processed']}\n"
369
+ table += f"Recommendation: {v['learned_adjustments']['recommendation']}\n"
370
+ table += "-" * 100 + "\n"
371
+
372
+ return table
373
+
374
+ except Exception as e:
375
+ print(f"[Model Versioning] Error generating comparison: {e}")
376
+ return f"Error: {e}"
377
+
378
+
379
+ def initialize_model_tracker():
380
+ """Initialize the model version tracker"""
381
+ return ModelVersionTracker()
382
+
383
+
384
+ # SQL for creating the required tables (run in Supabase Dashboard)
385
+ CREATE_TABLES_SQL = """
386
+ -- Table: model_versions
387
+ -- Stores each model version with parameters and thresholds
388
+ CREATE TABLE IF NOT EXISTS model_versions (
389
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
390
+ version_id VARCHAR(255) UNIQUE NOT NULL,
391
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
392
+ model_architecture VARCHAR(100) NOT NULL,
393
+ backbone VARCHAR(100),
394
+ parameters JSONB,
395
+ thresholds JSONB,
396
+ learned_adjustments JSONB,
397
+ training_metadata JSONB,
398
+ is_active BOOLEAN DEFAULT TRUE,
399
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
400
+ );
401
+
402
+ CREATE INDEX IF NOT EXISTS idx_model_versions_timestamp ON model_versions(timestamp DESC);
403
+ CREATE INDEX IF NOT EXISTS idx_model_versions_active ON model_versions(is_active) WHERE is_active = TRUE;
404
+
405
+ -- Table: training_history
406
+ -- Stores training cycle information with before/after comparisons
407
+ CREATE TABLE IF NOT EXISTS training_history (
408
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
409
+ cycle_id VARCHAR(255) UNIQUE NOT NULL,
410
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
411
+ before_version_id VARCHAR(255),
412
+ after_version_id VARCHAR(255),
413
+ feedback_samples_processed INTEGER,
414
+ feedback_patterns JSONB,
415
+ parameter_changes JSONB,
416
+ performance_metrics JSONB,
417
+ threshold_recommendation TEXT,
418
+ status VARCHAR(50),
419
+ notes TEXT,
420
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
421
+ );
422
+
423
+ CREATE INDEX IF NOT EXISTS idx_training_history_timestamp ON training_history(timestamp DESC);
424
+ CREATE INDEX IF NOT EXISTS idx_training_history_status ON training_history(status);
425
+
426
+ -- Foreign key constraints
427
+ ALTER TABLE training_history
428
+ ADD CONSTRAINT fk_before_version
429
+ FOREIGN KEY (before_version_id)
430
+ REFERENCES model_versions(version_id);
431
+
432
+ ALTER TABLE training_history
433
+ ADD CONSTRAINT fk_after_version
434
+ FOREIGN KEY (after_version_id)
435
+ REFERENCES model_versions(version_id);
436
+ """