mise en place gestion modele chatbot coté enseignant
This commit is contained in:
455
config/api_key_manager.php
Normal file
455
config/api_key_manager.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
136
sql/chatbot_api_management.sql
Normal file
136
sql/chatbot_api_management.sql
Normal 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
109
sql/update_chat_tables.sql
Normal 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;
|
||||
Reference in New Issue
Block a user