Spaces:
Sleeping
Add fine-tuning infrastructure with custom hyperparameters and dataset import
Browse filesDatabase 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 +14 -0
- app/models/models.py +88 -0
- app/routes/admin.py +273 -2
- app/templates/admin/base.html +6 -0
- app/templates/admin/training.html +652 -0
- requirements.txt +9 -0
|
@@ -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']
|
|
@@ -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 {}
|
|
@@ -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({
|
|
|
|
|
|
|
|
|
|
| 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
|
|
@@ -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">
|
|
@@ -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 %}
|
|
@@ -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
|