mise en place gestion modele chatbot coté enseignant

This commit is contained in:
nicoboy
2026-01-04 15:43:15 +01:00
parent b0d2a1d12e
commit 2d58bd0f5c
3 changed files with 700 additions and 0 deletions

455
config/api_key_manager.php Normal file
View File

@ -0,0 +1,455 @@
<?php
/**
* Gestionnaire de clés API pour le chatbot
* Chiffrement AES-256-CBC des clés API
*
* @author WebVal
* @date 2026-01-04
*/
class APIKeyManager {
private static $encryption_key = null;
private static $db = null;
/**
* Initialiser la connexion BDD
*/
private static function init() {
if (self::$db === null) {
self::$db = Database::getInstance();
}
}
/**
* Récupérer la clé de chiffrement
* Ordre de priorité : ENV > Fichier > Génération
*/
private static function getEncryptionKey() {
if (self::$encryption_key !== null) {
return self::$encryption_key;
}
// 1. Variable d'environnement
$key = getenv('WEBVAL_API_ENCRYPTION_KEY');
// 2. Fichier externe (hors webroot)
if (!$key && file_exists('/etc/webval/encryption.key')) {
$key = file_get_contents('/etc/webval/encryption.key');
}
// 3. Fichier dans config (fallback)
$keyFile = __DIR__ . '/../config/.encryption_key';
if (!$key && file_exists($keyFile)) {
$key = file_get_contents($keyFile);
}
// 4. Générer une nouvelle clé si aucune n'existe
if (!$key) {
$key = bin2hex(random_bytes(32));
// Essayer de sauvegarder
if (is_writable(__DIR__ . '/../config/')) {
file_put_contents($keyFile, $key);
chmod($keyFile, 0600);
}
error_log("⚠️ Nouvelle clé de chiffrement générée. Sauvegarde recommandée.");
}
self::$encryption_key = $key;
return $key;
}
/**
* Chiffrer une clé API
*
* @param string $plaintext Clé API en clair
* @return string Clé chiffrée (base64)
*/
public static function encrypt($plaintext) {
if (empty($plaintext)) {
return null;
}
$key = hash('sha256', self::getEncryptionKey(), true);
$iv = random_bytes(16);
$ciphertext = openssl_encrypt(
$plaintext,
'AES-256-CBC',
$key,
OPENSSL_RAW_DATA,
$iv
);
if ($ciphertext === false) {
throw new Exception("Erreur de chiffrement");
}
// Format: IV (16 bytes) + Ciphertext
return base64_encode($iv . $ciphertext);
}
/**
* Déchiffrer une clé API
*
* @param string $encrypted Clé chiffrée (base64)
* @return string|null Clé API en clair
*/
public static function decrypt($encrypted) {
if (empty($encrypted)) {
return null;
}
$key = hash('sha256', self::getEncryptionKey(), true);
$data = base64_decode($encrypted);
if ($data === false || strlen($data) < 16) {
throw new Exception("Données chiffrées invalides");
}
$iv = substr($data, 0, 16);
$ciphertext = substr($data, 16);
$plaintext = openssl_decrypt(
$ciphertext,
'AES-256-CBC',
$key,
OPENSSL_RAW_DATA,
$iv
);
if ($plaintext === false) {
throw new Exception("Erreur de déchiffrement");
}
return $plaintext;
}
/**
* Sauvegarder une clé API
*
* @param int $id_enseignant ID de l'enseignant
* @param string $provider Provider (gemini, mistral, etc.)
* @param string $api_key Clé API en clair
* @param string $model_default Modèle par défaut
* @return bool Succès
*/
public static function saveKey($id_enseignant, $provider, $api_key, $model_default = null) {
self::init();
// Chiffrer la clé
$encrypted = self::encrypt($api_key);
// Upsert (INSERT ... ON DUPLICATE KEY UPDATE)
$query = "INSERT INTO api_keys
(id_enseignant, provider, api_key_encrypted, model_default, is_active, quota_reset_date)
VALUES (?, ?, ?, ?, TRUE, DATE_ADD(CURDATE(), INTERVAL 1 MONTH))
ON DUPLICATE KEY UPDATE
api_key_encrypted = VALUES(api_key_encrypted),
model_default = VALUES(model_default),
is_active = TRUE,
updated_at = CURRENT_TIMESTAMP";
return self::$db->query($query, [
$id_enseignant,
$provider,
$encrypted,
$model_default
]);
}
/**
* Récupérer une clé API déchiffrée
*
* @param int $id_enseignant ID de l'enseignant
* @param string $provider Provider
* @return array|null ['api_key' => '...', 'model_default' => '...']
*/
public static function getKey($id_enseignant, $provider) {
self::init();
$result = self::$db->fetchOne(
"SELECT api_key_encrypted, model_default, is_active
FROM api_keys
WHERE id_enseignant = ? AND provider = ?",
[$id_enseignant, $provider]
);
if (!$result || !$result['is_active']) {
return null;
}
return [
'api_key' => self::decrypt($result['api_key_encrypted']),
'model_default' => $result['model_default']
];
}
/**
* Supprimer une clé API
*
* @param int $id_enseignant ID de l'enseignant
* @param string $provider Provider
* @return bool Succès
*/
public static function deleteKey($id_enseignant, $provider) {
self::init();
return self::$db->query(
"DELETE FROM api_keys WHERE id_enseignant = ? AND provider = ?",
[$id_enseignant, $provider]
);
}
/**
* Lister toutes les clés d'un enseignant
*
* @param int $id_enseignant ID de l'enseignant
* @return array Liste des clés (sans les clés déchiffrées)
*/
public static function listKeys($id_enseignant) {
self::init();
return self::$db->fetchAll(
"SELECT
id,
provider,
model_default,
is_active,
quota_max,
quota_used,
quota_reset_date,
last_used,
created_at
FROM api_keys
WHERE id_enseignant = ?
ORDER BY provider",
[$id_enseignant]
);
}
/**
* Incrémenter le quota utilisé
*
* @param int $id_enseignant ID de l'enseignant
* @param string $provider Provider
* @param int $tokens_used Nombre de tokens utilisés
*/
public static function incrementQuota($id_enseignant, $provider, $tokens_used = 1) {
self::init();
// Reset quota si date dépassée
self::$db->query(
"UPDATE api_keys
SET quota_used = 0, quota_reset_date = DATE_ADD(CURDATE(), INTERVAL 1 MONTH)
WHERE id_enseignant = ? AND provider = ? AND quota_reset_date < CURDATE()",
[$id_enseignant, $provider]
);
// Incrémenter
return self::$db->query(
"UPDATE api_keys
SET quota_used = quota_used + ?, last_used = NOW()
WHERE id_enseignant = ? AND provider = ?",
[$tokens_used, $id_enseignant, $provider]
);
}
/**
* Vérifier si le quota est dépassé
*
* @param int $id_enseignant ID de l'enseignant
* @param string $provider Provider
* @return bool True si quota OK
*/
public static function checkQuota($id_enseignant, $provider) {
self::init();
$result = self::$db->fetchOne(
"SELECT quota_max, quota_used FROM api_keys
WHERE id_enseignant = ? AND provider = ?",
[$id_enseignant, $provider]
);
if (!$result || $result['quota_max'] === null) {
return true; // Pas de limite
}
return $result['quota_used'] < $result['quota_max'];
}
/**
* Tester une connexion API
*
* @param string $provider Provider à tester
* @param string $api_key Clé API
* @param string $model Modèle à tester
* @return array ['success' => bool, 'message' => string, 'latency_ms' => int]
*/
public static function testConnection($provider, $api_key, $model = null) {
$start = microtime(true);
try {
switch ($provider) {
case 'gemini':
$result = self::testGemini($api_key, $model ?? 'gemini-2.0-flash-exp');
break;
case 'mistral':
$result = self::testMistral($api_key, $model ?? 'ministral-3b-latest');
break;
case 'openai':
$result = self::testOpenAI($api_key, $model ?? 'gpt-4o-mini');
break;
case 'ollama':
$result = self::testOllama($model ?? 'ministral-3:3b');
break;
default:
return ['success' => false, 'message' => 'Provider inconnu'];
}
$latency = round((microtime(true) - $start) * 1000);
$result['latency_ms'] = $latency;
return $result;
} catch (Exception $e) {
return [
'success' => false,
'message' => 'Erreur: ' . $e->getMessage(),
'latency_ms' => 0
];
}
}
/**
* Tester connexion Gemini
*/
private static function testGemini($api_key, $model) {
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$api_key}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => json_encode([
'contents' => [['parts' => [['text' => 'Test']]]]
])
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
return ['success' => true, 'message' => 'Connexion Gemini réussie'];
} elseif ($http_code === 400) {
return ['success' => false, 'message' => 'Clé API invalide'];
} elseif ($http_code === 429) {
return ['success' => false, 'message' => 'Quota dépassé'];
} else {
return ['success' => false, 'message' => "Erreur HTTP $http_code"];
}
}
/**
* Tester connexion Mistral
*/
private static function testMistral($api_key, $model) {
$ch = curl_init('https://api.mistral.ai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
],
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => json_encode([
'model' => $model,
'messages' => [['role' => 'user', 'content' => 'Test']],
'max_tokens' => 10
])
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
return ['success' => true, 'message' => 'Connexion Mistral réussie'];
} elseif ($http_code === 401) {
return ['success' => false, 'message' => 'Clé API invalide'];
} else {
return ['success' => false, 'message' => "Erreur HTTP $http_code"];
}
}
/**
* Tester connexion OpenAI
*/
private static function testOpenAI($api_key, $model) {
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
],
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => json_encode([
'model' => $model,
'messages' => [['role' => 'user', 'content' => 'Test']],
'max_tokens' => 10
])
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
return ['success' => true, 'message' => 'Connexion OpenAI réussie'];
} elseif ($http_code === 401) {
return ['success' => false, 'message' => 'Clé API invalide'];
} else {
return ['success' => false, 'message' => "Erreur HTTP $http_code"];
}
}
/**
* Tester connexion Ollama (local)
*/
private static function testOllama($model) {
$ch = curl_init('http://localhost:11434/api/generate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 5,
CURLOPT_POSTFIELDS => json_encode([
'model' => $model,
'prompt' => 'Test',
'stream' => false
])
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
return ['success' => true, 'message' => 'Connexion Ollama réussie'];
} else {
return ['success' => false, 'message' => 'Ollama non disponible'];
}
}
}

View File

@ -0,0 +1,136 @@
-- ============================================
-- WebVal - Gestion API Chatbot
-- Tables pour la gestion des clés API et modèles
-- Date: 2026-01-04
-- ============================================
-- Table des clés API des enseignants
CREATE TABLE IF NOT EXISTS api_keys (
id INT AUTO_INCREMENT PRIMARY KEY,
id_enseignant INT NOT NULL,
provider ENUM('gemini', 'mistral', 'openai', 'anthropic', 'ollama') NOT NULL,
api_key_encrypted TEXT, -- Clé chiffrée avec AES-256-CBC
model_default VARCHAR(100), -- Modèle par défaut (ex: 'gemini-2.0-flash-exp')
is_active BOOLEAN DEFAULT TRUE,
quota_max INT DEFAULT NULL, -- Limite mensuelle optionnelle
quota_used INT DEFAULT 0,
quota_reset_date DATE, -- Date de reset du quota
last_used TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (id_enseignant) REFERENCES utilisateurs(id_utilisateur) ON DELETE CASCADE,
UNIQUE KEY unique_provider (id_enseignant, provider),
INDEX idx_enseignant (id_enseignant),
INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table des modèles disponibles (catalogue)
CREATE TABLE IF NOT EXISTS available_models (
id INT AUTO_INCREMENT PRIMARY KEY,
provider VARCHAR(50) NOT NULL,
model_id VARCHAR(100) NOT NULL, -- ID technique du modèle
display_name VARCHAR(150) NOT NULL, -- Nom affiché
description TEXT,
context_length INT DEFAULT 4096, -- Taille contexte en tokens
is_free BOOLEAN DEFAULT FALSE,
cost_per_1m_tokens DECIMAL(10,4) DEFAULT NULL, -- Coût par million de tokens
recommended BOOLEAN DEFAULT FALSE,
speed_rating TINYINT DEFAULT 3, -- 1=lent, 5=très rapide
quality_rating TINYINT DEFAULT 3, -- 1=basique, 5=excellent
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_model (provider, model_id),
INDEX idx_provider (provider),
INDEX idx_recommended (recommended),
INDEX idx_active (active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table de configuration chatbot par évaluation
CREATE TABLE IF NOT EXISTS chatbot_eval_config (
id INT AUTO_INCREMENT PRIMARY KEY,
id_evaluation INT NOT NULL,
id_enseignant INT NOT NULL,
provider_default VARCHAR(50) DEFAULT 'ollama',
model_default VARCHAR(100) DEFAULT 'ministral-3:3b',
allow_student_override BOOLEAN DEFAULT FALSE, -- L'élève peut-il choisir ?
max_messages_per_question INT DEFAULT 3,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (id_evaluation) REFERENCES evaluations(id_evaluation) ON DELETE CASCADE,
FOREIGN KEY (id_enseignant) REFERENCES utilisateurs(id_utilisateur) ON DELETE CASCADE,
UNIQUE KEY unique_eval (id_evaluation),
INDEX idx_enseignant (id_enseignant)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table de configuration par élève (override)
CREATE TABLE IF NOT EXISTS chatbot_student_config (
id INT AUTO_INCREMENT PRIMARY KEY,
id_evaluation INT NOT NULL,
id_eleve INT NOT NULL,
provider_override VARCHAR(50),
model_override VARCHAR(100),
created_by INT NOT NULL, -- ID enseignant qui a fait l'override
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (id_evaluation) REFERENCES evaluations(id_evaluation) ON DELETE CASCADE,
FOREIGN KEY (id_eleve) REFERENCES utilisateurs(id_utilisateur) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES utilisateurs(id_utilisateur) ON DELETE CASCADE,
UNIQUE KEY unique_student_eval (id_evaluation, id_eleve),
INDEX idx_evaluation (id_evaluation),
INDEX idx_eleve (id_eleve)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Pré-remplir les modèles disponibles
INSERT INTO available_models
(provider, model_id, display_name, description, context_length, is_free, cost_per_1m_tokens, recommended, speed_rating, quality_rating)
VALUES
-- Gemini (Google)
('gemini', 'gemini-2.0-flash-exp', 'Gemini 2.0 Flash (Expérimental)', 'Modèle ultra-rapide de Google, gratuit avec quotas généreux', 1000000, TRUE, 0, TRUE, 5, 4),
('gemini', 'gemini-1.5-flash', 'Gemini 1.5 Flash', 'Modèle rapide et équilibré', 1000000, TRUE, 0, FALSE, 4, 4),
('gemini', 'gemini-1.5-pro', 'Gemini 1.5 Pro', 'Modèle le plus puissant de Google', 2000000, FALSE, 1.25, FALSE, 3, 5),
-- Mistral
('mistral', 'mistral-small-latest', 'Mistral Small', 'Modèle compact et rapide', 32000, FALSE, 0.20, FALSE, 5, 3),
('mistral', 'ministral-3b-latest', 'Ministral 3B', 'Petit modèle efficace pour le raisonnement', 128000, FALSE, 0.04, TRUE, 5, 3),
('mistral', 'ministral-8b-latest', 'Ministral 8B', 'Meilleur équilibre qualité/vitesse', 128000, FALSE, 0.10, TRUE, 4, 4),
('mistral', 'mistral-large-latest', 'Mistral Large', 'Modèle le plus puissant de Mistral', 128000, FALSE, 2.00, FALSE, 3, 5),
-- OpenAI
('openai', 'gpt-4o-mini', 'GPT-4o Mini', 'Petit modèle rapide et économique', 128000, FALSE, 0.15, TRUE, 5, 4),
('openai', 'gpt-4o', 'GPT-4o', 'Modèle multimodal puissant', 128000, FALSE, 2.50, FALSE, 4, 5),
('openai', 'gpt-4-turbo', 'GPT-4 Turbo', 'Modèle GPT-4 optimisé', 128000, FALSE, 10.00, FALSE, 3, 5),
-- Ollama (local)
('ollama', 'ministral-3:3b', 'Ministral 3B (Local)', 'Modèle local rapide', 128000, TRUE, 0, TRUE, 4, 3),
('ollama', 'qwen2-math:1.5b', 'Qwen2 Math 1.5B (Local)', 'Spécialisé en mathématiques', 32000, TRUE, 0, FALSE, 5, 3),
('ollama', 'llama3.2:3b', 'Llama 3.2 3B (Local)', 'Modèle généraliste compact', 128000, TRUE, 0, FALSE, 3, 4);
-- Ajouter colonne provider à la table chat_conversations pour tracking
ALTER TABLE chat_conversations
ADD COLUMN IF NOT EXISTS provider VARCHAR(50) DEFAULT 'ollama',
ADD COLUMN IF NOT EXISTS model_used VARCHAR(100) DEFAULT 'ministral-3:3b',
ADD COLUMN IF NOT EXISTS api_response_time_ms INT DEFAULT NULL,
ADD INDEX idx_provider (provider);
-- Ajouter colonne provider à la table chat_messages pour analytics
ALTER TABLE chat_messages
ADD COLUMN IF NOT EXISTS api_cost DECIMAL(10,6) DEFAULT 0.000000;
-- Vue pour les stats d'utilisation enseignant
CREATE OR REPLACE VIEW v_chatbot_usage_stats AS
SELECT
cc.id_enseignant,
cc.provider,
cc.model_used,
COUNT(DISTINCT cc.id_conversation) as total_conversations,
COUNT(cm.id) as total_messages,
AVG(cc.api_response_time_ms) as avg_response_time_ms,
SUM(cm.api_cost) as total_cost,
DATE_FORMAT(cc.date_debut, '%Y-%m') as month
FROM chat_conversations cc
LEFT JOIN chat_messages cm ON cc.id_conversation = cm.id_conversation
GROUP BY cc.id_enseignant, cc.provider, cc.model_used, month;
-- ============================================
-- FIN DU SCRIPT
-- ============================================

109
sql/update_chat_tables.sql Normal file
View File

@ -0,0 +1,109 @@
-- ============================================
-- WebVal - Mise à jour chat_conversations
-- Ajout colonnes pour tracking API (sécurisé)
-- ============================================
-- Désactiver les erreurs temporairement
SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0;
-- Ajouter colonnes une par une (ignore si existe déjà)
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'chat_conversations'
AND column_name = 'provider') > 0,
'SELECT ''Column provider already exists''',
'ALTER TABLE chat_conversations ADD COLUMN provider VARCHAR(50) DEFAULT ''ollama'''
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'chat_conversations'
AND column_name = 'model_used') > 0,
'SELECT ''Column model_used already exists''',
'ALTER TABLE chat_conversations ADD COLUMN model_used VARCHAR(100) DEFAULT ''ministral-3:3b'''
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'chat_conversations'
AND column_name = 'api_response_time_ms') > 0,
'SELECT ''Column api_response_time_ms already exists''',
'ALTER TABLE chat_conversations ADD COLUMN api_response_time_ms INT DEFAULT NULL'
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'chat_conversations'
AND column_name = 'id_enseignant') > 0,
'SELECT ''Column id_enseignant already exists''',
'ALTER TABLE chat_conversations ADD COLUMN id_enseignant INT DEFAULT NULL'
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Index
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'chat_conversations'
AND index_name = 'idx_provider') > 0,
'SELECT ''Index idx_provider already exists''',
'ALTER TABLE chat_conversations ADD INDEX idx_provider (provider)'
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Foreign key (plus complexe, on teste avec SHOW CREATE TABLE)
SET @fk_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE table_schema = DATABASE()
AND table_name = 'chat_conversations'
AND referenced_table_name = 'utilisateurs'
AND column_name = 'id_enseignant');
SET @s = IF(@fk_exists > 0,
'SELECT ''Foreign key already exists''',
'ALTER TABLE chat_conversations ADD FOREIGN KEY (id_enseignant) REFERENCES utilisateurs(id_utilisateur) ON DELETE SET NULL'
);
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Colonne api_cost dans chat_messages
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'chat_messages'
AND column_name = 'api_cost') > 0,
'SELECT ''Column api_cost already exists''',
'ALTER TABLE chat_messages ADD COLUMN api_cost DECIMAL(10,6) DEFAULT 0.000000'
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Restaurer les notes SQL
SET SQL_NOTES=@OLD_SQL_NOTES;
-- Remplir id_enseignant rétroactivement
UPDATE chat_conversations cc
JOIN tentatives_eleves te ON cc.id_tentative = te.id_tentative
JOIN evaluations e ON te.id_evaluation = e.id_evaluation
SET cc.id_enseignant = e.id_enseignant
WHERE cc.id_enseignant IS NULL;
SELECT '✅ Colonnes ajoutées avec succès' as Status;