thadillo Claude commited on
Commit
19ce9e8
·
1 Parent(s): 7b3a4a2

Add fine-tuning infrastructure with custom hyperparameters and dataset import

Browse files

Database Models:
- Created TrainingExample model to store admin corrections
- Created FineTuningRun model to track training runs and results

Training Dashboard:
- New admin/training page with statistics and controls
- Custom hyperparameter inputs (LoRA rank, learning rate, epochs, batch size, alpha, dropout)
- Category distribution chart
- Training history table
- Import training dataset functionality
- Progress tracking modals

API Endpoints:
- GET /admin/training - Training dashboard
- GET /admin/api/training-stats - Training statistics
- GET /admin/api/training-examples - List training examples
- DELETE /admin/api/training-example/<id> - Delete training example
- POST /admin/import-training-dataset - Import standalone training dataset

Export/Import:
- Updated export_json to include trainingExamples
- Updated import_data to restore training examples
- Added standalone training dataset import for pre-labeled data

Update Category Tracking:
- Modified update_category endpoint to automatically capture training examples
- Tracks original predictions and admin corrections
- Creates/updates TrainingExample records on every category change

Dependencies:
- Added peft, datasets, scikit-learn, matplotlib, seaborn, accelerate, evaluate

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

app/fine_tuning/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fine-tuning module for training custom classification models.
3
+
4
+ This module provides tools for:
5
+ - Preparing training datasets from admin corrections
6
+ - Fine-tuning BART models using LoRA (Low-Rank Adaptation)
7
+ - Evaluating model performance
8
+ - Managing model versions and deployment
9
+ """
10
+
11
+ from .trainer import BARTFineTuner
12
+ from .model_manager import ModelManager
13
+
14
+ __all__ = ['BARTFineTuner', 'ModelManager']
app/models/models.py CHANGED
@@ -1,5 +1,6 @@
1
  from app import db
2
  from datetime import datetime
 
3
 
4
  class Token(db.Model):
5
  __tablename__ = 'tokens'
@@ -66,3 +67,90 @@ class Settings(db.Model):
66
  setting = Settings(key=key, value=value)
67
  db.session.add(setting)
68
  db.session.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from app import db
2
  from datetime import datetime
3
+ import json
4
 
5
  class Token(db.Model):
6
  __tablename__ = 'tokens'
 
67
  setting = Settings(key=key, value=value)
68
  db.session.add(setting)
69
  db.session.commit()
70
+
71
+
72
+ class TrainingExample(db.Model):
73
+ """Stores admin corrections for model fine-tuning"""
74
+ __tablename__ = 'training_examples'
75
+
76
+ id = db.Column(db.Integer, primary_key=True)
77
+ submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
78
+ message = db.Column(db.Text, nullable=False) # Snapshot of submission text
79
+ original_category = db.Column(db.String(50), nullable=True) # AI's prediction
80
+ corrected_category = db.Column(db.String(50), nullable=False) # Admin's correction
81
+ contributor_type = db.Column(db.String(20), nullable=False)
82
+ correction_timestamp = db.Column(db.DateTime, default=datetime.utcnow)
83
+ confidence_score = db.Column(db.Float, nullable=True) # Original prediction confidence
84
+ used_in_training = db.Column(db.Boolean, default=False)
85
+ training_run_id = db.Column(db.Integer, db.ForeignKey('fine_tuning_runs.id'), nullable=True)
86
+
87
+ # Relationships
88
+ submission = db.relationship('Submission', backref='training_examples')
89
+ training_run = db.relationship('FineTuningRun', backref='training_examples')
90
+
91
+ def to_dict(self):
92
+ return {
93
+ 'id': self.id,
94
+ 'submission_id': self.submission_id,
95
+ 'message': self.message,
96
+ 'original_category': self.original_category,
97
+ 'corrected_category': self.corrected_category,
98
+ 'contributor_type': self.contributor_type,
99
+ 'correction_timestamp': self.correction_timestamp.isoformat() if self.correction_timestamp else None,
100
+ 'confidence_score': self.confidence_score,
101
+ 'used_in_training': self.used_in_training,
102
+ 'training_run_id': self.training_run_id,
103
+ 'is_correction': self.original_category != self.corrected_category if self.original_category else False
104
+ }
105
+
106
+
107
+ class FineTuningRun(db.Model):
108
+ """Tracks fine-tuning training runs and their results"""
109
+ __tablename__ = 'fine_tuning_runs'
110
+
111
+ id = db.Column(db.Integer, primary_key=True)
112
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
113
+ status = db.Column(db.String(20), default='preparing') # preparing, training, evaluating, completed, failed
114
+ num_training_examples = db.Column(db.Integer, nullable=True)
115
+ num_validation_examples = db.Column(db.Integer, nullable=True)
116
+ num_test_examples = db.Column(db.Integer, nullable=True)
117
+ training_config = db.Column(db.Text, nullable=True) # JSON string
118
+ results = db.Column(db.Text, nullable=True) # JSON string with metrics
119
+ model_path = db.Column(db.String(255), nullable=True)
120
+ is_active_model = db.Column(db.Boolean, default=False)
121
+ improvement_over_baseline = db.Column(db.Float, nullable=True)
122
+ completed_at = db.Column(db.DateTime, nullable=True)
123
+ error_message = db.Column(db.Text, nullable=True)
124
+
125
+ def to_dict(self):
126
+ return {
127
+ 'id': self.id,
128
+ 'created_at': self.created_at.isoformat() if self.created_at else None,
129
+ 'status': self.status,
130
+ 'num_training_examples': self.num_training_examples,
131
+ 'num_validation_examples': self.num_validation_examples,
132
+ 'num_test_examples': self.num_test_examples,
133
+ 'training_config': json.loads(self.training_config) if self.training_config else None,
134
+ 'results': json.loads(self.results) if self.results else None,
135
+ 'model_path': self.model_path,
136
+ 'is_active_model': self.is_active_model,
137
+ 'improvement_over_baseline': self.improvement_over_baseline,
138
+ 'completed_at': self.completed_at.isoformat() if self.completed_at else None,
139
+ 'error_message': self.error_message
140
+ }
141
+
142
+ def set_config(self, config_dict):
143
+ """Set training config from dict"""
144
+ self.training_config = json.dumps(config_dict)
145
+
146
+ def get_config(self):
147
+ """Get training config as dict"""
148
+ return json.loads(self.training_config) if self.training_config else {}
149
+
150
+ def set_results(self, results_dict):
151
+ """Set results from dict"""
152
+ self.results = json.dumps(results_dict)
153
+
154
+ def get_results(self):
155
+ """Get results as dict"""
156
+ return json.loads(self.results) if self.results else {}
app/routes/admin.py CHANGED
@@ -1,5 +1,5 @@
1
  from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify, send_file
2
- from app.models.models import Token, Submission, Settings
3
  from app import db
4
  from app.analyzer import get_analyzer
5
  from functools import wraps
@@ -226,6 +226,10 @@ def update_category(submission_id):
226
  submission = Submission.query.get_or_404(submission_id)
227
  data = request.json
228
  category = data.get('category')
 
 
 
 
229
 
230
  # Convert empty string to None
231
  if category == '' or category == 'null':
@@ -235,8 +239,33 @@ def update_category(submission_id):
235
  if category and category not in CATEGORIES:
236
  return jsonify({'success': False, 'error': f'Invalid category: {category}'}), 400
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  submission.category = category
239
  db.session.commit()
 
240
  return jsonify({'success': True, 'category': category})
241
 
242
  except Exception as e:
@@ -307,6 +336,7 @@ def export_json():
307
  data = {
308
  'tokens': [t.to_dict() for t in Token.query.all()],
309
  'submissions': [s.to_dict() for s in Submission.query.all()],
 
310
  'submissionOpen': Settings.get_setting('submission_open', 'true') == 'true',
311
  'tokenGenerationEnabled': Settings.get_setting('token_generation_enabled', 'true') == 'true',
312
  'exportDate': datetime.utcnow().isoformat()
@@ -401,13 +431,35 @@ def import_data():
401
  )
402
  db.session.add(submission)
403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  # Import settings
405
  Settings.set_setting('submission_open', 'true' if data.get('submissionOpen', True) else 'false')
406
  Settings.set_setting('token_generation_enabled', 'true' if data.get('tokenGenerationEnabled', True) else 'false')
407
 
408
  db.session.commit()
409
 
410
- return jsonify({'success': True})
 
 
 
411
 
412
  except Exception as e:
413
  db.session.rollback()
@@ -435,3 +487,222 @@ def clear_all_data():
435
  except Exception as e:
436
  db.session.rollback()
437
  return jsonify({'success': False, 'error': str(e)}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify, send_file
2
+ from app.models.models import Token, Submission, Settings, TrainingExample, FineTuningRun
3
  from app import db
4
  from app.analyzer import get_analyzer
5
  from functools import wraps
 
226
  submission = Submission.query.get_or_404(submission_id)
227
  data = request.json
228
  category = data.get('category')
229
+ confidence = data.get('confidence') # Optional: frontend can pass prediction confidence
230
+
231
+ # Store original category before change
232
+ original_category = submission.category
233
 
234
  # Convert empty string to None
235
  if category == '' or category == 'null':
 
239
  if category and category not in CATEGORIES:
240
  return jsonify({'success': False, 'error': f'Invalid category: {category}'}), 400
241
 
242
+ # Create training example if admin is making a correction or confirmation
243
+ if category is not None: # Only track when assigning a category
244
+ # Check if training example already exists for this submission
245
+ existing_example = TrainingExample.query.filter_by(submission_id=submission_id).first()
246
+
247
+ if existing_example:
248
+ # Update existing example
249
+ existing_example.original_category = original_category
250
+ existing_example.corrected_category = category
251
+ existing_example.correction_timestamp = datetime.utcnow()
252
+ existing_example.confidence_score = confidence
253
+ else:
254
+ # Create new training example
255
+ training_example = TrainingExample(
256
+ submission_id=submission_id,
257
+ message=submission.message,
258
+ original_category=original_category,
259
+ corrected_category=category,
260
+ contributor_type=submission.contributor_type,
261
+ confidence_score=confidence
262
+ )
263
+ db.session.add(training_example)
264
+
265
+ # Update submission category
266
  submission.category = category
267
  db.session.commit()
268
+
269
  return jsonify({'success': True, 'category': category})
270
 
271
  except Exception as e:
 
336
  data = {
337
  'tokens': [t.to_dict() for t in Token.query.all()],
338
  'submissions': [s.to_dict() for s in Submission.query.all()],
339
+ 'trainingExamples': [ex.to_dict() for ex in TrainingExample.query.all()],
340
  'submissionOpen': Settings.get_setting('submission_open', 'true') == 'true',
341
  'tokenGenerationEnabled': Settings.get_setting('token_generation_enabled', 'true') == 'true',
342
  'exportDate': datetime.utcnow().isoformat()
 
431
  )
432
  db.session.add(submission)
433
 
434
+ # Import training examples if present
435
+ training_examples_imported = 0
436
+ for ex_data in data.get('trainingExamples', []):
437
+ # Find corresponding submission by message (or create placeholder)
438
+ submission = Submission.query.filter_by(message=ex_data['message']).first()
439
+ if submission:
440
+ training_example = TrainingExample(
441
+ submission_id=submission.id,
442
+ message=ex_data['message'],
443
+ original_category=ex_data.get('original_category'),
444
+ corrected_category=ex_data['corrected_category'],
445
+ contributor_type=ex_data['contributor_type'],
446
+ correction_timestamp=datetime.fromisoformat(ex_data['correction_timestamp']) if ex_data.get('correction_timestamp') else datetime.utcnow(),
447
+ confidence_score=ex_data.get('confidence_score'),
448
+ used_in_training=ex_data.get('used_in_training', False)
449
+ )
450
+ db.session.add(training_example)
451
+ training_examples_imported += 1
452
+
453
  # Import settings
454
  Settings.set_setting('submission_open', 'true' if data.get('submissionOpen', True) else 'false')
455
  Settings.set_setting('token_generation_enabled', 'true' if data.get('tokenGenerationEnabled', True) else 'false')
456
 
457
  db.session.commit()
458
 
459
+ return jsonify({
460
+ 'success': True,
461
+ 'training_examples_imported': training_examples_imported
462
+ })
463
 
464
  except Exception as e:
465
  db.session.rollback()
 
487
  except Exception as e:
488
  db.session.rollback()
489
  return jsonify({'success': False, 'error': str(e)}), 500
490
+
491
+
492
+ # ============================================================================
493
+ # FINE-TUNING & TRAINING DATA ENDPOINTS
494
+ # ============================================================================
495
+
496
+ @bp.route('/training')
497
+ @admin_required
498
+ def training_dashboard():
499
+ """Display the fine-tuning training dashboard"""
500
+ # Get training statistics
501
+ total_examples = TrainingExample.query.count()
502
+ corrections_count = TrainingExample.query.filter(
503
+ TrainingExample.original_category != TrainingExample.corrected_category
504
+ ).count()
505
+ confirmations_count = total_examples - corrections_count
506
+
507
+ # Category distribution
508
+ from sqlalchemy import func
509
+ category_distribution = db.session.query(
510
+ TrainingExample.corrected_category,
511
+ func.count(TrainingExample.id)
512
+ ).group_by(TrainingExample.corrected_category).all()
513
+
514
+ category_stats = {cat: 0 for cat in CATEGORIES}
515
+ for cat, count in category_distribution:
516
+ if cat in category_stats:
517
+ category_stats[cat] = count
518
+
519
+ # Get all training runs
520
+ training_runs = FineTuningRun.query.order_by(FineTuningRun.created_at.desc()).all()
521
+
522
+ # Get active model
523
+ active_model = FineTuningRun.query.filter_by(is_active_model=True).first()
524
+
525
+ # Fine-tuning settings
526
+ min_training_examples = int(Settings.get_setting('min_training_examples', '20'))
527
+ fine_tuning_enabled = Settings.get_setting('fine_tuning_enabled', 'true') == 'true'
528
+
529
+ return render_template('admin/training.html',
530
+ total_examples=total_examples,
531
+ corrections_count=corrections_count,
532
+ confirmations_count=confirmations_count,
533
+ category_stats=category_stats,
534
+ categories=CATEGORIES,
535
+ training_runs=training_runs,
536
+ active_model=active_model,
537
+ min_training_examples=min_training_examples,
538
+ fine_tuning_enabled=fine_tuning_enabled,
539
+ ready_to_train=total_examples >= min_training_examples)
540
+
541
+
542
+ @bp.route('/api/training-stats', methods=['GET'])
543
+ @admin_required
544
+ def get_training_stats():
545
+ """Get training data statistics (API endpoint)"""
546
+ total_examples = TrainingExample.query.count()
547
+ corrections_count = TrainingExample.query.filter(
548
+ TrainingExample.original_category != TrainingExample.corrected_category
549
+ ).count()
550
+
551
+ # Category distribution
552
+ from sqlalchemy import func
553
+ category_distribution = db.session.query(
554
+ TrainingExample.corrected_category,
555
+ func.count(TrainingExample.id)
556
+ ).group_by(TrainingExample.corrected_category).all()
557
+
558
+ category_stats = {cat: 0 for cat in CATEGORIES}
559
+ for cat, count in category_distribution:
560
+ if cat in category_stats:
561
+ category_stats[cat] = count
562
+
563
+ # Check for data quality issues
564
+ duplicates = db.session.query(
565
+ TrainingExample.message,
566
+ func.count(TrainingExample.id)
567
+ ).group_by(TrainingExample.message).having(func.count(TrainingExample.id) > 1).count()
568
+
569
+ min_examples = int(Settings.get_setting('min_training_examples', '20'))
570
+ min_per_category = min(category_stats.values()) if category_stats.values() else 0
571
+
572
+ return jsonify({
573
+ 'total_examples': total_examples,
574
+ 'corrections_count': corrections_count,
575
+ 'confirmations_count': total_examples - corrections_count,
576
+ 'category_stats': category_stats,
577
+ 'duplicates_count': duplicates,
578
+ 'min_examples_threshold': min_examples,
579
+ 'min_examples_per_category': min_per_category,
580
+ 'ready_to_train': total_examples >= min_examples and min_per_category >= 2
581
+ })
582
+
583
+
584
+ @bp.route('/api/training-examples', methods=['GET'])
585
+ @admin_required
586
+ def get_training_examples():
587
+ """Get all training examples"""
588
+ page = request.args.get('page', 1, type=int)
589
+ per_page = request.args.get('per_page', 50, type=int)
590
+ category_filter = request.args.get('category', 'all')
591
+ corrections_only = request.args.get('corrections_only', 'false') == 'true'
592
+
593
+ query = TrainingExample.query
594
+
595
+ if category_filter != 'all':
596
+ query = query.filter_by(corrected_category=category_filter)
597
+
598
+ if corrections_only:
599
+ query = query.filter(TrainingExample.original_category != TrainingExample.corrected_category)
600
+
601
+ query = query.order_by(TrainingExample.correction_timestamp.desc())
602
+
603
+ pagination = query.paginate(page=page, per_page=per_page, error_out=False)
604
+
605
+ return jsonify({
606
+ 'examples': [ex.to_dict() for ex in pagination.items],
607
+ 'total': pagination.total,
608
+ 'pages': pagination.pages,
609
+ 'current_page': page
610
+ })
611
+
612
+
613
+ @bp.route('/api/training-example/<int:example_id>', methods=['DELETE'])
614
+ @admin_required
615
+ def delete_training_example(example_id):
616
+ """Delete a training example"""
617
+ try:
618
+ example = TrainingExample.query.get_or_404(example_id)
619
+
620
+ # Don't allow deleting if already used in training
621
+ if example.used_in_training:
622
+ return jsonify({
623
+ 'success': False,
624
+ 'error': 'Cannot delete example already used in training run'
625
+ }), 400
626
+
627
+ db.session.delete(example)
628
+ db.session.commit()
629
+
630
+ return jsonify({'success': True})
631
+
632
+ except Exception as e:
633
+ db.session.rollback()
634
+ return jsonify({'success': False, 'error': str(e)}), 500
635
+
636
+
637
+ @bp.route('/import-training-dataset', methods=['POST'])
638
+ @admin_required
639
+ def import_training_dataset():
640
+ """Import standalone training dataset (just training examples, not full session)"""
641
+ if 'file' not in request.files:
642
+ return jsonify({'success': False, 'error': 'No file uploaded'}), 400
643
+
644
+ file = request.files['file']
645
+
646
+ if file.filename == '':
647
+ return jsonify({'success': False, 'error': 'No file selected'}), 400
648
+
649
+ try:
650
+ data = json.load(file)
651
+
652
+ # Support both formats: array of examples or object with 'trainingExamples' key
653
+ training_data = data if isinstance(data, list) else data.get('trainingExamples', [])
654
+
655
+ imported_count = 0
656
+
657
+ for ex_data in training_data:
658
+ # Check if training example already exists (by message)
659
+ existing = TrainingExample.query.filter_by(message=ex_data['message']).first()
660
+
661
+ if existing:
662
+ # Update existing example
663
+ existing.original_category = ex_data.get('original_category')
664
+ existing.corrected_category = ex_data['corrected_category']
665
+ existing.contributor_type = ex_data.get('contributor_type', 'other')
666
+ existing.correction_timestamp = datetime.utcnow()
667
+ existing.confidence_score = ex_data.get('confidence_score')
668
+ else:
669
+ # Create placeholder submission if needed
670
+ submission = Submission.query.filter_by(message=ex_data['message']).first()
671
+
672
+ if not submission:
673
+ # Create placeholder submission for this training example
674
+ submission = Submission(
675
+ message=ex_data['message'],
676
+ contributor_type=ex_data.get('contributor_type', 'other'),
677
+ category=ex_data.get('corrected_category'),
678
+ timestamp=datetime.utcnow()
679
+ )
680
+ db.session.add(submission)
681
+ db.session.flush() # Get submission ID
682
+
683
+ # Create new training example
684
+ training_example = TrainingExample(
685
+ submission_id=submission.id,
686
+ message=ex_data['message'],
687
+ original_category=ex_data.get('original_category'),
688
+ corrected_category=ex_data['corrected_category'],
689
+ contributor_type=ex_data.get('contributor_type', 'other'),
690
+ confidence_score=ex_data.get('confidence_score')
691
+ )
692
+ db.session.add(training_example)
693
+
694
+ imported_count += 1
695
+
696
+ db.session.commit()
697
+
698
+ return jsonify({
699
+ 'success': True,
700
+ 'imported_count': imported_count
701
+ })
702
+
703
+ except KeyError as e:
704
+ db.session.rollback()
705
+ return jsonify({'success': False, 'error': f'Missing required field: {str(e)}'}), 400
706
+ except Exception as e:
707
+ db.session.rollback()
708
+ return jsonify({'success': False, 'error': str(e)}), 500
app/templates/admin/base.html CHANGED
@@ -42,6 +42,12 @@
42
  <i class="bi bi-graph-up"></i> Analytics
43
  </a>
44
  </li>
 
 
 
 
 
 
45
  </ul>
46
  <div class="d-flex gap-2">
47
  <a href="{{ url_for('admin.export_json') }}" class="btn btn-success btn-sm">
 
42
  <i class="bi bi-graph-up"></i> Analytics
43
  </a>
44
  </li>
45
+ <li class="nav-item">
46
+ <a class="nav-link {% if request.endpoint == 'admin.training_dashboard' %}active{% endif %}"
47
+ href="{{ url_for('admin.training_dashboard') }}">
48
+ <i class="bi bi-robot"></i> Training
49
+ </a>
50
+ </li>
51
  </ul>
52
  <div class="d-flex gap-2">
53
  <a href="{{ url_for('admin.export_json') }}" class="btn btn-success btn-sm">
app/templates/admin/training.html ADDED
@@ -0,0 +1,652 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin/base.html" %}
2
+
3
+ {% block title %}Model Training - Admin Dashboard{% endblock %}
4
+
5
+ {% block admin_content %}
6
+ <div class="mb-4">
7
+ <h2><i class="bi bi-robot"></i> Model Fine-Tuning</h2>
8
+ <p class="text-muted">Train the AI model with admin corrections to improve classification accuracy</p>
9
+ </div>
10
+
11
+ <!-- Training Data Statistics -->
12
+ <div class="row g-4 mb-4">
13
+ <div class="col-md-3">
14
+ <div class="card shadow-sm">
15
+ <div class="card-body text-center">
16
+ <h3 class="text-primary">{{ total_examples }}</h3>
17
+ <p class="text-muted mb-0">Total Training Examples</p>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ <div class="col-md-3">
22
+ <div class="card shadow-sm">
23
+ <div class="card-body text-center">
24
+ <h3 class="text-warning">{{ corrections_count }}</h3>
25
+ <p class="text-muted mb-0">AI Corrections</p>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ <div class="col-md-3">
30
+ <div class="card shadow-sm">
31
+ <div class="card-body text-center">
32
+ <h3 class="text-success">{{ confirmations_count }}</h3>
33
+ <p class="text-muted mb-0">AI Confirmations</p>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <div class="col-md-3">
38
+ <div class="card shadow-sm {% if ready_to_train %}border-success{% endif %}">
39
+ <div class="card-body text-center">
40
+ <h3 class="{% if ready_to_train %}text-success{% else %}text-secondary{% endif %}">
41
+ {{ min_training_examples }}
42
+ </h3>
43
+ <p class="text-muted mb-0">
44
+ Minimum Required
45
+ {% if ready_to_train %}
46
+ <i class="bi bi-check-circle-fill text-success"></i>
47
+ {% endif %}
48
+ </p>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- Category Distribution Chart -->
55
+ <div class="card shadow-sm mb-4">
56
+ <div class="card-header">
57
+ <h5 class="mb-0"><i class="bi bi-bar-chart-fill"></i> Category Distribution</h5>
58
+ </div>
59
+ <div class="card-body">
60
+ <canvas id="categoryDistChart" height="80"></canvas>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Fine-Tuning Controls -->
65
+ <div class="card shadow-sm mb-4">
66
+ <div class="card-header d-flex justify-content-between align-items-center">
67
+ <h5 class="mb-0"><i class="bi bi-gear-fill"></i> Training Controls</h5>
68
+ {% if active_model %}
69
+ <span class="badge bg-success">
70
+ <i class="bi bi-cpu-fill"></i> Fine-tuned model active
71
+ </span>
72
+ {% else %}
73
+ <span class="badge bg-secondary">
74
+ <i class="bi bi-cpu"></i> Base model active
75
+ </span>
76
+ {% endif %}
77
+ </div>
78
+ <div class="card-body">
79
+ <!-- Import Training Dataset Section -->
80
+ <div class="mb-4">
81
+ <h6><i class="bi bi-upload"></i> Import Training Dataset</h6>
82
+ <p class="text-muted small">Upload a JSON file with pre-labeled training examples</p>
83
+ <div class="input-group">
84
+ <input type="file" class="form-control" id="trainingDatasetFile" accept=".json">
85
+ <button class="btn btn-outline-secondary" type="button" onclick="importTrainingDataset()">
86
+ <i class="bi bi-cloud-upload"></i> Import
87
+ </button>
88
+ </div>
89
+ </div>
90
+ <hr>
91
+
92
+ {% if not fine_tuning_enabled %}
93
+ <div class="alert alert-warning">
94
+ <i class="bi bi-exclamation-triangle-fill"></i>
95
+ Fine-tuning is currently disabled in settings.
96
+ </div>
97
+ {% elif not ready_to_train %}
98
+ <div class="alert alert-info">
99
+ <i class="bi bi-info-circle-fill"></i>
100
+ Collect at least {{ min_training_examples }} training examples before starting fine-tuning.
101
+ Current: {{ total_examples }}
102
+ </div>
103
+ {% else %}
104
+ <div class="alert alert-success">
105
+ <i class="bi bi-check-circle-fill"></i>
106
+ Ready to train! You have {{ total_examples }} training examples collected.
107
+ </div>
108
+
109
+ <form id="trainingConfigForm">
110
+ <div class="row mb-3">
111
+ <div class="col-md-4">
112
+ <label class="form-label">Training Split (%)</label>
113
+ <input type="number" class="form-control" id="trainSplit" value="70" min="50" max="80">
114
+ </div>
115
+ <div class="col-md-4">
116
+ <label class="form-label">Validation Split (%)</label>
117
+ <input type="number" class="form-control" id="valSplit" value="15" min="10" max="30">
118
+ </div>
119
+ <div class="col-md-4">
120
+ <label class="form-label">Test Split (%)</label>
121
+ <input type="number" class="form-control" id="testSplit" value="15" min="10" max="30" readonly>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="row mb-3">
126
+ <div class="col-md-4">
127
+ <label class="form-label">
128
+ LoRA Rank
129
+ <button type="button" class="btn btn-sm btn-link p-0" onclick="toggleCustomLoraRank()">
130
+ <i class="bi bi-pencil-square"></i>
131
+ </button>
132
+ </label>
133
+ <select class="form-select" id="loraRank" onchange="checkCustomLoraRank()">
134
+ <option value="8">8 (Fast, less capacity)</option>
135
+ <option value="16" selected>16 (Balanced)</option>
136
+ <option value="32">32 (Slow, more capacity)</option>
137
+ <option value="custom">Custom...</option>
138
+ </select>
139
+ <input type="number" class="form-control mt-2" id="customLoraRank"
140
+ style="display: none;" placeholder="Enter custom rank (4-64)"
141
+ min="4" max="64" step="4" value="16">
142
+ </div>
143
+ <div class="col-md-4">
144
+ <label class="form-label">
145
+ Learning Rate
146
+ <button type="button" class="btn btn-sm btn-link p-0" onclick="toggleCustomLearningRate()">
147
+ <i class="bi bi-pencil-square"></i>
148
+ </button>
149
+ </label>
150
+ <select class="form-select" id="learningRate" onchange="checkCustomLearningRate()">
151
+ <option value="1e-4">1e-4 (Conservative)</option>
152
+ <option value="3e-4" selected>3e-4 (Recommended)</option>
153
+ <option value="5e-4">5e-4 (Aggressive)</option>
154
+ <option value="custom">Custom...</option>
155
+ </select>
156
+ <input type="text" class="form-control mt-2" id="customLearningRate"
157
+ style="display: none;" placeholder="e.g., 2e-4"
158
+ pattern="[0-9]+\.?[0-9]*e-[0-9]+" value="3e-4">
159
+ </div>
160
+ <div class="col-md-4">
161
+ <label class="form-label">
162
+ Epochs
163
+ <button type="button" class="btn btn-sm btn-link p-0" onclick="toggleCustomEpochs()">
164
+ <i class="bi bi-pencil-square"></i>
165
+ </button>
166
+ </label>
167
+ <select class="form-select" id="numEpochs" onchange="checkCustomEpochs()">
168
+ <option value="3" selected>3 (Fast)</option>
169
+ <option value="5">5 (Balanced)</option>
170
+ <option value="8">8 (Thorough)</option>
171
+ <option value="custom">Custom...</option>
172
+ </select>
173
+ <input type="number" class="form-control mt-2" id="customEpochs"
174
+ style="display: none;" placeholder="Enter custom epochs (1-20)"
175
+ min="1" max="20" value="3">
176
+ </div>
177
+ </div>
178
+
179
+ <div class="row mb-3">
180
+ <div class="col-md-4">
181
+ <label class="form-label">Batch Size</label>
182
+ <select class="form-select" id="batchSize">
183
+ <option value="4">4 (Low memory)</option>
184
+ <option value="8" selected>8 (Recommended)</option>
185
+ <option value="16">16 (High memory)</option>
186
+ </select>
187
+ </div>
188
+ <div class="col-md-4">
189
+ <label class="form-label">LoRA Alpha</label>
190
+ <input type="number" class="form-control" id="loraAlpha" value="32" min="8" max="128" step="8">
191
+ <small class="text-muted">Scaling factor (typically 2x rank)</small>
192
+ </div>
193
+ <div class="col-md-4">
194
+ <label class="form-label">LoRA Dropout</label>
195
+ <input type="number" class="form-control" id="loraDropout" value="0.1" min="0" max="0.5" step="0.05">
196
+ <small class="text-muted">Regularization (0.0-0.5)</small>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="d-grid gap-2">
201
+ <button type="button" class="btn btn-primary btn-lg" onclick="startTraining()">
202
+ <i class="bi bi-play-circle-fill"></i> Start Fine-Tuning
203
+ </button>
204
+ </div>
205
+ </form>
206
+ {% endif %}
207
+
208
+ {% if active_model %}
209
+ <hr>
210
+ <div class="d-grid gap-2">
211
+ <button class="btn btn-warning" onclick="rollbackModel()">
212
+ <i class="bi bi-arrow-counterclockwise"></i> Rollback to Base Model
213
+ </button>
214
+ </div>
215
+ {% endif %}
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Training History -->
220
+ <div class="card shadow-sm mb-4">
221
+ <div class="card-header">
222
+ <h5 class="mb-0"><i class="bi bi-clock-history"></i> Training History</h5>
223
+ </div>
224
+ <div class="card-body">
225
+ {% if training_runs %}
226
+ <div class="table-responsive">
227
+ <table class="table table-hover">
228
+ <thead>
229
+ <tr>
230
+ <th>Run ID</th>
231
+ <th>Date</th>
232
+ <th>Status</th>
233
+ <th>Examples</th>
234
+ <th>Accuracy</th>
235
+ <th>Improvement</th>
236
+ <th>Actions</th>
237
+ </tr>
238
+ </thead>
239
+ <tbody>
240
+ {% for run in training_runs %}
241
+ <tr {% if run.is_active_model %}class="table-success"{% endif %}>
242
+ <td>
243
+ #{{ run.id }}
244
+ {% if run.is_active_model %}
245
+ <span class="badge bg-success ms-2">Active</span>
246
+ {% endif %}
247
+ </td>
248
+ <td>{{ run.created_at.strftime('%Y-%m-%d %H:%M') if run.created_at else 'N/A' }}</td>
249
+ <td>
250
+ {% if run.status == 'completed' %}
251
+ <span class="badge bg-success">Completed</span>
252
+ {% elif run.status == 'failed' %}
253
+ <span class="badge bg-danger">Failed</span>
254
+ {% elif run.status == 'training' %}
255
+ <span class="badge bg-primary">Training...</span>
256
+ {% else %}
257
+ <span class="badge bg-secondary">{{ run.status.title() }}</span>
258
+ {% endif %}
259
+ </td>
260
+ <td>{{ run.num_training_examples or 'N/A' }}</td>
261
+ <td>
262
+ {% if run.results %}
263
+ {{ "%.1f"|format((run.get_results().get('test_accuracy', 0) * 100)) }}%
264
+ {% else %}
265
+ N/A
266
+ {% endif %}
267
+ </td>
268
+ <td>
269
+ {% if run.improvement_over_baseline %}
270
+ <span class="{% if run.improvement_over_baseline > 0 %}text-success{% else %}text-danger{% endif %}">
271
+ {{ "%+.1f"|format(run.improvement_over_baseline * 100) }}%
272
+ </span>
273
+ {% else %}
274
+ N/A
275
+ {% endif %}
276
+ </td>
277
+ <td>
278
+ {% if run.status == 'completed' and not run.is_active_model %}
279
+ <button class="btn btn-sm btn-primary" onclick="deployModel({{ run.id }})">
280
+ <i class="bi bi-cloud-upload"></i> Deploy
281
+ </button>
282
+ {% endif %}
283
+ <button class="btn btn-sm btn-info" onclick="viewRunDetails({{ run.id }})">
284
+ <i class="bi bi-eye"></i> Details
285
+ </button>
286
+ </td>
287
+ </tr>
288
+ {% endfor %}
289
+ </tbody>
290
+ </table>
291
+ </div>
292
+ {% else %}
293
+ <div class="text-center py-4 text-muted">
294
+ <i class="bi bi-inbox" style="font-size: 3rem;"></i>
295
+ <p class="mt-3">No training runs yet. Start your first fine-tuning session above!</p>
296
+ </div>
297
+ {% endif %}
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Training Progress Modal -->
302
+ <div class="modal fade" id="trainingProgressModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
303
+ <div class="modal-dialog modal-dialog-centered">
304
+ <div class="modal-content">
305
+ <div class="modal-header">
306
+ <h5 class="modal-title"><i class="bi bi-hourglass-split"></i> Training in Progress</h5>
307
+ </div>
308
+ <div class="modal-body text-center">
309
+ <div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;" role="status">
310
+ <span class="visually-hidden">Training...</span>
311
+ </div>
312
+ <h5 id="trainingStatus">Preparing data...</h5>
313
+ <p class="text-muted" id="trainingDetails">This may take several minutes</p>
314
+ <div class="progress mt-3">
315
+ <div class="progress-bar progress-bar-striped progress-bar-animated"
316
+ role="progressbar"
317
+ style="width: 0%"
318
+ id="trainingProgress"></div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+ <!-- Run Details Modal -->
326
+ <div class="modal fade" id="runDetailsModal" tabindex="-1">
327
+ <div class="modal-dialog modal-lg">
328
+ <div class="modal-content">
329
+ <div class="modal-header">
330
+ <h5 class="modal-title"><i class="bi bi-info-circle"></i> Training Run Details</h5>
331
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
332
+ </div>
333
+ <div class="modal-body" id="runDetailsContent">
334
+ <!-- Content loaded dynamically -->
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
341
+
342
+ <script>
343
+ // Category distribution chart
344
+ const categoryStats = {{ category_stats|tojson }};
345
+ const categories = {{ categories|tojson }};
346
+
347
+ const ctx = document.getElementById('categoryDistChart').getContext('2d');
348
+ new Chart(ctx, {
349
+ type: 'bar',
350
+ data: {
351
+ labels: categories,
352
+ datasets: [{
353
+ label: 'Training Examples',
354
+ data: categories.map(cat => categoryStats[cat] || 0),
355
+ backgroundColor: [
356
+ 'rgba(59, 130, 246, 0.7)', // Vision - blue
357
+ 'rgba(239, 68, 68, 0.7)', // Problem - red
358
+ 'rgba(16, 185, 129, 0.7)', // Objectives - green
359
+ 'rgba(245, 158, 11, 0.7)', // Directives - orange
360
+ 'rgba(139, 92, 246, 0.7)', // Values - purple
361
+ 'rgba(236, 72, 153, 0.7)' // Actions - pink
362
+ ],
363
+ borderColor: [
364
+ 'rgba(59, 130, 246, 1)',
365
+ 'rgba(239, 68, 68, 1)',
366
+ 'rgba(16, 185, 129, 1)',
367
+ 'rgba(245, 158, 11, 1)',
368
+ 'rgba(139, 92, 246, 1)',
369
+ 'rgba(236, 72, 153, 1)'
370
+ ],
371
+ borderWidth: 2
372
+ }]
373
+ },
374
+ options: {
375
+ responsive: true,
376
+ plugins: {
377
+ legend: {
378
+ display: false
379
+ }
380
+ },
381
+ scales: {
382
+ y: {
383
+ beginAtZero: true,
384
+ ticks: {
385
+ stepSize: 1
386
+ }
387
+ }
388
+ }
389
+ }
390
+ });
391
+
392
+ // Update test split automatically
393
+ document.getElementById('trainSplit').addEventListener('input', updateTestSplit);
394
+ document.getElementById('valSplit').addEventListener('input', updateTestSplit);
395
+
396
+ function updateTestSplit() {
397
+ const train = parseInt(document.getElementById('trainSplit').value);
398
+ const val = parseInt(document.getElementById('valSplit').value);
399
+ const test = 100 - train - val;
400
+ document.getElementById('testSplit').value = test;
401
+ }
402
+
403
+ // Custom hyperparameter toggle functions
404
+ function checkCustomLoraRank() {
405
+ const select = document.getElementById('loraRank');
406
+ const customInput = document.getElementById('customLoraRank');
407
+ customInput.style.display = select.value === 'custom' ? 'block' : 'none';
408
+ }
409
+
410
+ function toggleCustomLoraRank() {
411
+ document.getElementById('loraRank').value = 'custom';
412
+ checkCustomLoraRank();
413
+ document.getElementById('customLoraRank').focus();
414
+ }
415
+
416
+ function checkCustomLearningRate() {
417
+ const select = document.getElementById('learningRate');
418
+ const customInput = document.getElementById('customLearningRate');
419
+ customInput.style.display = select.value === 'custom' ? 'block' : 'none';
420
+ }
421
+
422
+ function toggleCustomLearningRate() {
423
+ document.getElementById('learningRate').value = 'custom';
424
+ checkCustomLearningRate();
425
+ document.getElementById('customLearningRate').focus();
426
+ }
427
+
428
+ function checkCustomEpochs() {
429
+ const select = document.getElementById('numEpochs');
430
+ const customInput = document.getElementById('customEpochs');
431
+ customInput.style.display = select.value === 'custom' ? 'block' : 'none';
432
+ }
433
+
434
+ function toggleCustomEpochs() {
435
+ document.getElementById('numEpochs').value = 'custom';
436
+ checkCustomEpochs();
437
+ document.getElementById('customEpochs').focus();
438
+ }
439
+
440
+ // Get hyperparameter values (custom or preset)
441
+ function getLoraRank() {
442
+ const select = document.getElementById('loraRank');
443
+ if (select.value === 'custom') {
444
+ return parseInt(document.getElementById('customLoraRank').value);
445
+ }
446
+ return parseInt(select.value);
447
+ }
448
+
449
+ function getLearningRate() {
450
+ const select = document.getElementById('learningRate');
451
+ if (select.value === 'custom') {
452
+ return parseFloat(document.getElementById('customLearningRate').value);
453
+ }
454
+ return parseFloat(select.value);
455
+ }
456
+
457
+ function getNumEpochs() {
458
+ const select = document.getElementById('numEpochs');
459
+ if (select.value === 'custom') {
460
+ return parseInt(document.getElementById('customEpochs').value);
461
+ }
462
+ return parseInt(select.value);
463
+ }
464
+
465
+ // Import training dataset function
466
+ function importTrainingDataset() {
467
+ const fileInput = document.getElementById('trainingDatasetFile');
468
+ const file = fileInput.files[0];
469
+
470
+ if (!file) {
471
+ alert('Please select a JSON file to import');
472
+ return;
473
+ }
474
+
475
+ if (!confirm('Import training dataset? This will add new training examples to the existing collection.')) {
476
+ return;
477
+ }
478
+
479
+ const formData = new FormData();
480
+ formData.append('file', file);
481
+
482
+ fetch('{{ url_for("admin.import_training_dataset") }}', {
483
+ method: 'POST',
484
+ body: formData
485
+ })
486
+ .then(response => response.json())
487
+ .then(data => {
488
+ if (data.success) {
489
+ alert(`Successfully imported ${data.imported_count} training examples!`);
490
+ location.reload();
491
+ } else {
492
+ alert('Error importing dataset: ' + data.error);
493
+ }
494
+ })
495
+ .catch(err => {
496
+ alert('Error: ' + err.message);
497
+ });
498
+ }
499
+
500
+ // Start training function
501
+ function startTraining() {
502
+ if (!confirm('Start fine-tuning the model? This will take several minutes.')) {
503
+ return;
504
+ }
505
+
506
+ const config = {
507
+ train_split: parseInt(document.getElementById('trainSplit').value) / 100,
508
+ val_split: parseInt(document.getElementById('valSplit').value) / 100,
509
+ test_split: parseInt(document.getElementById('testSplit').value) / 100,
510
+ lora_rank: getLoraRank(),
511
+ lora_alpha: parseInt(document.getElementById('loraAlpha').value),
512
+ lora_dropout: parseFloat(document.getElementById('loraDropout').value),
513
+ learning_rate: getLearningRate(),
514
+ num_epochs: getNumEpochs(),
515
+ batch_size: parseInt(document.getElementById('batchSize').value)
516
+ };
517
+
518
+ // Show progress modal
519
+ const progressModal = new bootstrap.Modal(document.getElementById('trainingProgressModal'));
520
+ progressModal.show();
521
+
522
+ fetch('{{ url_for("admin.start_fine_tuning") }}', {
523
+ method: 'POST',
524
+ headers: {'Content-Type': 'application/json'},
525
+ body: JSON.stringify(config)
526
+ })
527
+ .then(response => response.json())
528
+ .then(data => {
529
+ if (data.success) {
530
+ // Poll for training status
531
+ pollTrainingStatus(data.run_id, progressModal);
532
+ } else {
533
+ progressModal.hide();
534
+ alert('Error starting training: ' + data.error);
535
+ }
536
+ })
537
+ .catch(err => {
538
+ progressModal.hide();
539
+ alert('Error: ' + err.message);
540
+ });
541
+ }
542
+
543
+ // Poll training status
544
+ let pollInterval;
545
+ function pollTrainingStatus(runId, modal) {
546
+ pollInterval = setInterval(() => {
547
+ fetch(`{{ url_for("admin.get_training_status", run_id=0) }}`.replace('/0', `/${runId}`))
548
+ .then(response => response.json())
549
+ .then(data => {
550
+ document.getElementById('trainingStatus').textContent = data.status_message || data.status;
551
+ document.getElementById('trainingDetails').textContent = data.details || '';
552
+
553
+ // Update progress bar
554
+ const progress = data.progress || 0;
555
+ document.getElementById('trainingProgress').style.width = progress + '%';
556
+
557
+ if (data.status === 'completed' || data.status === 'failed') {
558
+ clearInterval(pollInterval);
559
+ modal.hide();
560
+
561
+ if (data.status === 'completed') {
562
+ alert('Training completed! Accuracy: ' + (data.results.test_accuracy * 100).toFixed(1) + '%');
563
+ } else {
564
+ alert('Training failed: ' + data.error_message);
565
+ }
566
+
567
+ location.reload();
568
+ }
569
+ });
570
+ }, 2000); // Poll every 2 seconds
571
+ }
572
+
573
+ // Deploy model
574
+ function deployModel(runId) {
575
+ if (!confirm('Deploy this model? It will replace the currently active model.')) {
576
+ return;
577
+ }
578
+
579
+ fetch(`{{ url_for("admin.deploy_model", run_id=0) }}`.replace('/0', `/${runId}`), {
580
+ method: 'POST'
581
+ })
582
+ .then(response => response.json())
583
+ .then(data => {
584
+ if (data.success) {
585
+ alert('Model deployed successfully!');
586
+ location.reload();
587
+ } else {
588
+ alert('Error deploying model: ' + data.error);
589
+ }
590
+ });
591
+ }
592
+
593
+ // Rollback model
594
+ function rollbackModel() {
595
+ if (!confirm('Rollback to the base model? The fine-tuned model will be deactivated.')) {
596
+ return;
597
+ }
598
+
599
+ fetch('{{ url_for("admin.rollback_model") }}', {
600
+ method: 'POST'
601
+ })
602
+ .then(response => response.json())
603
+ .then(data => {
604
+ if (data.success) {
605
+ alert('Rolled back to base model');
606
+ location.reload();
607
+ } else {
608
+ alert('Error: ' + data.error);
609
+ }
610
+ });
611
+ }
612
+
613
+ // View run details
614
+ function viewRunDetails(runId) {
615
+ fetch(`{{ url_for("admin.get_run_details", run_id=0) }}`.replace('/0', `/${runId}`))
616
+ .then(response => response.json())
617
+ .then(data => {
618
+ const content = `
619
+ <div class="row">
620
+ <div class="col-md-6">
621
+ <h6>Training Configuration</h6>
622
+ <ul class="list-group">
623
+ <li class="list-group-item"><strong>LoRA Rank:</strong> ${data.config.lora_rank}</li>
624
+ <li class="list-group-item"><strong>Learning Rate:</strong> ${data.config.learning_rate}</li>
625
+ <li class="list-group-item"><strong>Epochs:</strong> ${data.config.num_epochs}</li>
626
+ <li class="list-group-item"><strong>Training Examples:</strong> ${data.num_training_examples}</li>
627
+ <li class="list-group-item"><strong>Validation Examples:</strong> ${data.num_validation_examples}</li>
628
+ <li class="list-group-item"><strong>Test Examples:</strong> ${data.num_test_examples}</li>
629
+ </ul>
630
+ </div>
631
+ <div class="col-md-6">
632
+ <h6>Results</h6>
633
+ ${data.results ? `
634
+ <ul class="list-group">
635
+ <li class="list-group-item"><strong>Test Accuracy:</strong> ${(data.results.test_accuracy * 100).toFixed(1)}%</li>
636
+ <li class="list-group-item"><strong>Training Loss:</strong> ${data.results.train_loss ? data.results.train_loss.toFixed(4) : 'N/A'}</li>
637
+ <li class="list-group-item"><strong>Validation Loss:</strong> ${data.results.val_loss ? data.results.val_loss.toFixed(4) : 'N/A'}</li>
638
+ <li class="list-group-item"><strong>Improvement:</strong> <span class="${data.improvement_over_baseline > 0 ? 'text-success' : 'text-danger'}">${(data.improvement_over_baseline * 100).toFixed(1)}%</span></li>
639
+ </ul>
640
+ ` : '<p class="text-muted">No results available</p>'}
641
+ </div>
642
+ </div>
643
+ ${data.error_message ? `<div class="alert alert-danger mt-3">${data.error_message}</div>` : ''}
644
+ `;
645
+
646
+ document.getElementById('runDetailsContent').innerHTML = content;
647
+ const modal = new bootstrap.Modal(document.getElementById('runDetailsModal'));
648
+ modal.show();
649
+ });
650
+ }
651
+ </script>
652
+ {% endblock %}
requirements.txt CHANGED
@@ -5,3 +5,12 @@ transformers==4.36.0
5
  torch==2.5.0
6
  sentencepiece>=0.2.0
7
  gunicorn==21.2.0
 
 
 
 
 
 
 
 
 
 
5
  torch==2.5.0
6
  sentencepiece>=0.2.0
7
  gunicorn==21.2.0
8
+
9
+ # Fine-tuning dependencies
10
+ peft>=0.7.0
11
+ datasets>=2.14.0
12
+ scikit-learn>=1.3.0
13
+ matplotlib>=3.7.0
14
+ seaborn>=0.12.0
15
+ accelerate>=0.24.0
16
+ evaluate>=0.4.0