Docfile commited on
Commit
b7844e1
·
verified ·
1 Parent(s): e1feafa

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -443
app.py CHANGED
@@ -1,464 +1,171 @@
1
- # -*- coding: utf-8 -*- # Bonne pratique d'ajouter l'encodage
2
-
3
- # Importations nécessaires
4
- from flask import Flask, request, render_template_string, jsonify, redirect, url_for
5
- import requests
6
- import threading
7
- import uuid # Pour générer des identifiants uniques pour chaque tâche
8
- import time
9
- import copy # Pour copier le payload pour chaque requête
10
- from typing import Dict, Any, List, Tuple, Optional # Pour les annotations de type
11
-
12
- # --- Configuration ---
13
- TARGET_URL: str = "https://hook.us1.make.com/zal5qn0ggbewmvtsbo2uenfno8tz3n56"
14
- BASE_PAYLOAD: Dict[str, Any] = {
15
- "name": "Testeur Auto ",
16
- "email": "[email protected]", # Ajout de '+auto' pour distinguer
17
- "company": "aragon Inc.",
18
- "message": "Ceci est un test automatisé via Flask.",
19
- "date": "2023-10-27T10:30:00Z", # Tu pourrais rendre cette date dynamique si besoin
20
- "source": "http://simulateur-bsbs-flask.com"
21
- }
22
-
23
- # Constantes pour les statuts (évite les fautes de frappe)
24
- STATUS_STARTING: str = 'starting'
25
- STATUS_RUNNING: str = 'running'
26
- STATUS_COMPLETED: str = 'completed'
27
- STATUS_FAILED: str = 'failed'
28
- # Optional: STATUS_COMPLETED_WITH_ERRORS: str = 'completed_with_errors'
29
-
30
- # Structure pour stocker l'état des tâches (jobs) en mémoire
31
- # Format: { 'job_id': {'status': str, 'total': int, 'completed_count': int, 'error_count': int, 'errors': List[Dict[str, Any]] } }
32
- jobs: Dict[str, Dict[str, Any]] = {}
33
- jobs_lock = threading.Lock() # Pour éviter les problèmes d'accès concurrents au dict jobs
34
-
35
- app = Flask(__name__) # Correction de l'initialisation de Flask
36
-
37
- # --- Templates HTML (inchangés, car ils semblent corrects) ---
38
-
39
- # Page d'accueil pour démarrer les requêtes
40
- HTML_INDEX = """
41
- <!DOCTYPE html>
42
- <html lang="fr">
43
- <head>
44
- <meta charset="UTF-8">
45
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
46
- <title>Lanceur de Requêtes</title>
47
- <style>
48
- body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
49
- h1, h2 { color: #555; }
50
- label { display: block; margin-bottom: 5px; font-weight: bold; }
51
- input[type=number] { width: 100px; padding: 8px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 4px; }
52
- button { padding: 10px 15px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px; }
53
- button:hover { background-color: #0056b3; }
54
- .error { color: red; margin-top: 10px; font-weight: bold; }
55
- ul { list-style: none; padding: 0; }
56
- li { background-color: #fff; margin-bottom: 10px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
57
- li a { text-decoration: none; color: #007bff; }
58
- li a:hover { text-decoration: underline; }
59
- .job-info { font-size: 0.9em; color: #666; }
60
- </style>
61
- </head>
62
- <body>
63
- <h1>Envoyer des Requêtes POST en Masse</h1>
64
- <form method="POST" action="{{ url_for('start_requests') }}">
65
- <label for="num_requests">Nombre de requêtes à envoyer :</label>
66
- <input type="number" id="num_requests" name="num_requests" min="1" required value="10">
67
- <button type="submit">Lancer les requêtes</button>
68
- </form>
69
- {% if error %}
70
- <p class="error">{{ error }}</p>
71
- {% endif %}
72
-
73
- <h2>Tâches récentes :</h2>
74
- <ul>
75
- {% for job_id, job_info in jobs_list.items() %}
76
- <li>
77
- <a href="{{ url_for('job_status', job_id=job_id) }}">Tâche {{ job_id }}</a>
78
- <span class="job-info">
79
- (Statut : {{ job_info.status }}, {{ job_info.completed_count }}/{{ job_info.total }} requêtes traitées, {{ job_info.error_count }} erreurs)
80
- </span>
81
- </li>
82
- {% else %}
83
- <li>Aucune tâche récente.</li>
84
- {% endfor %}
85
- </ul>
86
- </body>
87
- </html>
88
- """
89
-
90
- # Page pour suivre la progression d'une tâche spécifique
91
- HTML_STATUS = """
92
- <!DOCTYPE html>
93
- <html lang="fr">
94
- <head>
95
- <meta charset="UTF-8">
96
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
- <title>Statut Tâche {{ job_id }}</title>
98
- <style>
99
- body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
100
- h1 { color: #555; }
101
- #progress-bar-container { width: 100%; background-color: #e0e0e0; border-radius: 5px; margin-bottom: 10px; overflow: hidden; } /* Overflow hidden for border radius */
102
- #progress-bar { width: 0%; height: 30px; background-color: #28a745; /* Green default */ text-align: center; line-height: 30px; color: white; border-radius: 0; /* Radius handled by container */ transition: width 0.5s ease-in-out, background-color 0.5s ease-in-out; }
103
- .error-log { margin-top: 15px; max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9; border-radius: 4px;}
104
- .error-log h3 { margin-top: 0; }
105
- .error-log p { margin: 5px 0; font-size: 0.9em; color: #dc3545; } /* Red error text */
106
- .status-message { font-weight: bold; margin-bottom: 15px; padding: 10px; background-color: #e9ecef; border-radius: 4px; }
107
- a { color: #007bff; text-decoration: none; }
108
- a:hover { text-decoration: underline; }
109
- </style>
110
- </head>
111
- <body>
112
- <h1>Statut de la Tâche : {{ job_id }}</h1>
113
- <div id="status-message" class="status-message">Chargement des informations...</div>
114
- <div id="progress-bar-container">
115
- <div id="progress-bar">0%</div>
116
- </div>
117
- <p>Requêtes traitées : <span id="completed">0</span> / <span id="total">?</span></p>
118
- <p>Erreurs rencontrées : <span id="errors">0</span></p>
119
- <div id="error-details" class="error-log" style="display: none;">
120
- <h3>Détails des erreurs :</h3>
121
- <div id="error-list"></div>
122
- </div>
123
- <p><a href="{{ url_for('index') }}">Retour à l'accueil</a></p>
124
-
125
- <script>
126
- const jobId = "{{ job_id }}";
127
- const statusMessageEl = document.getElementById('status-message');
128
- const progressBarEl = document.getElementById('progress-bar');
129
- const completedEl = document.getElementById('completed');
130
- const totalEl = document.getElementById('total');
131
- const errorsEl = document.getElementById('errors');
132
- const errorDetailsEl = document.getElementById('error-details');
133
- const errorListEl = document.getElementById('error-list');
134
-
135
- let intervalId = null;
136
- let lastStatus = ""; // Pour éviter des mises à jour inutiles si le statut n'a pas changé
137
-
138
- function updateStatus() {
139
- fetch(`/api/status/${jobId}`)
140
- .then(response => {
141
- if (!response.ok) {
142
- // Si 404, la tâche n'existe pas (ou plus), arrêter les requêtes
143
- if (response.status === 404) {
144
- statusMessageEl.textContent = "Erreur: Tâche non trouvée.";
145
- throw new Error('Job Not Found'); // Pour arrêter l'intervalle dans le catch
146
- }
147
- throw new Error(`Erreur HTTP: ${response.status}`);
148
- }
149
- return response.json();
150
- })
151
- .then(data => {
152
- if (!data) { // Devrait être géré par le 404 mais double sécurité
153
- statusMessageEl.textContent = "En attente des données...";
154
- return;
155
- }
156
-
157
- // Comparer avec le statut précédent pour éviter de redessiner si rien n'a changé
158
- const currentDataSignature = `${data.status}-${data.completed_count}-${data.error_count}`;
159
- if (currentDataSignature === lastStatus) {
160
- // console.log("Pas de changement de statut détecté.");
161
- return; // Rien à faire
162
- }
163
- lastStatus = currentDataSignature;
164
-
165
- // Mise à jour des éléments DOM
166
- completedEl.textContent = data.completed_count;
167
- totalEl.textContent = data.total;
168
- errorsEl.textContent = data.error_count;
169
- statusMessageEl.textContent = `Statut : ${data.status}`; // Utilise les constantes définies côté serveur
170
-
171
- let percentage = 0;
172
- if (data.total > 0) {
173
- // On calcule le pourcentage sur le nombre total de requêtes à faire
174
- percentage = Math.round((data.completed_count / data.total) * 100);
175
- }
176
- progressBarEl.style.width = percentage + '%';
177
- progressBarEl.textContent = percentage + '%';
178
-
179
- // Afficher les erreurs s'il y en a
180
- if (data.error_count > 0 && data.errors && data.errors.length > 0) {
181
- errorListEl.innerHTML = ''; // Vider les erreurs précédentes
182
- data.errors.forEach(err => {
183
- const p = document.createElement('p');
184
- // Utilisation de textContent pour éviter les injections XSS potentielles si les messages d'erreur contenaient du HTML
185
- p.textContent = `Req ${err.index}: ${err.error}`;
186
- errorListEl.appendChild(p);
187
- });
188
- errorDetailsEl.style.display = 'block';
189
- } else {
190
- errorDetailsEl.style.display = 'none';
191
- }
192
-
193
- // Gérer la fin de la tâche
194
- if (data.status === '{{ STATUS_COMPLETED }}' || data.status === '{{ STATUS_FAILED }}') {
195
- if (intervalId) {
196
- clearInterval(intervalId);
197
- intervalId = null; // Arrête les mises à jour futures
198
- app.logger.info(f"Mises à jour automatiques arrêtées pour la tâche {jobId} (statut final: {data.status}).");
199
- }
200
- // Mettre à jour la couleur de la barre de progression en fonction du résultat final
201
- if (data.status === '{{ STATUS_COMPLETED }}' && data.error_count == 0) {
202
- progressBarEl.style.backgroundColor = '#28a745'; // Vert succès
203
- } else if (data.status === '{{ STATUS_COMPLETED }}' && data.error_count > 0) {
204
- progressBarEl.style.backgroundColor = '#ffc107'; // Jaune/Orange pour succès avec erreurs
205
- } else { // STATUS_FAILED
206
- progressBarEl.style.backgroundColor = '#dc3545'; // Rouge échec
207
- }
208
- } else {
209
- // Si la tâche est en cours, s'assurer que la couleur est celle par défaut (ou une couleur "en cours")
210
- progressBarEl.style.backgroundColor = '#007bff'; // Bleu pour en cours
211
- }
212
- })
213
- .catch(error => {
214
- console.error("Erreur lors de la récupération du statut:", error);
215
- if (error.message !== 'Job Not Found') { // Ne pas écraser le message "Tâche non trouvée"
216
- statusMessageEl.textContent = "Erreur lors de la récupération du statut.";
217
- }
218
- // Arrêter les mises à jour en cas d'erreur persistante ou de tâche non trouvée
219
- if (intervalId) {
220
- clearInterval(intervalId);
221
- intervalId = null;
222
- app.logger.error(f"Arrêt des mises à jour pour la tâche {jobId} suite à une erreur: {error}");
223
- }
224
- // Optionnel: Changer la couleur de la barre en cas d'erreur de récupération
225
- progressBarEl.style.backgroundColor = '#6c757d'; // Gris pour état indéterminé/erreur
226
- });
227
- }
228
-
229
- // Démarrer la mise à jour : première mise à jour immédiate, puis toutes les 2 secondes
230
- updateStatus();
231
- if (!intervalId && statusMessageEl.textContent !== "Erreur: Tâche non trouvée.") { // Ne pas démarrer l'intervalle si la tâche n'est déjà pas trouvée
232
- intervalId = setInterval(updateStatus, 2000);
233
- }
234
- </script>
235
- </body>
236
- </html>
237
- """
238
-
239
- # --- Fonctions Logiques ---
240
-
241
- def send_single_request(target_url: str, payload: Dict[str, Any], job_id: str, request_index: int) -> Tuple[bool, Optional[str]]:
242
- """
243
- Envoie UNE requête POST unique à la cible.
244
-
245
- Args:
246
- target_url: L'URL à laquelle envoyer la requête.
247
- payload: Le dictionnaire de base du payload.
248
- job_id: L'ID de la tâche parente (pour logs/debug).
249
- request_index: L'index de cette requête dans la tâche (0-based).
250
-
251
- Returns:
252
- Un tuple (succès: bool, message_erreur: Optional[str]).
253
- """
254
- # Crée une copie profonde pour éviter de modifier l'original entre les threads
255
- # et pour ajouter des informations spécifiques à cette requête.
256
- current_payload = copy.deepcopy(payload)
257
- # Personnalisation du message pour identifier la requête spécifique
258
- current_payload['message'] += f" (Requête {request_index + 1} / Job {job_id})"
259
- # Ajout d'un UUID unique à chaque requête pour un suivi fin si nécessaire côté serveur cible
260
- current_payload['request_uuid'] = str(uuid.uuid4())
261
- # Optionnel: Mettre à jour la date/heure au moment de l'envoi
262
- # from datetime import datetime, timezone
263
- # current_payload['date'] = datetime.now(timezone.utc).isoformat()
264
-
265
  try:
266
- response = requests.post(target_url, json=current_payload, timeout=30) # Timeout de 30 secondes
267
- response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx, 5xx)
268
- # app.logger.debug(f"Job {job_id} - Requête {request_index + 1}: Succès (Status {response.status_code})")
269
- return True, None # Succès
270
- except requests.exceptions.Timeout:
271
- error_msg = f"Timeout après 30s pour Req {request_index + 1}"
272
- app.logger.warning(f"Job {job_id}: {error_msg}")
273
- return False, error_msg
274
- except requests.exceptions.HTTPError as e:
275
- error_msg = f"Erreur HTTP {e.response.status_code} pour Req {request_index + 1}: {e.response.reason}"
276
- app.logger.warning(f"Job {job_id}: {error_msg} - Réponse: {e.response.text[:200]}") # Log début de réponse
277
- return False, error_msg
278
- except requests.exceptions.RequestException as e:
279
- # Capture les autres erreurs (DNS, connexion refusée, etc.)
280
- error_msg = f"Erreur Réseau/Requête pour Req {request_index + 1}: {str(e)}"
281
- app.logger.error(f"Job {job_id}: {error_msg}")
282
- return False, error_msg # Échec avec message d'erreur
283
-
284
- def background_task(job_id: str, num_requests: int, target_url: str, base_payload: Dict[str, Any]):
285
- """
286
- Fonction exécutée dans un thread séparé pour envoyer toutes les requêtes d'une tâche.
287
- Met à jour l'état de la tâche dans le dictionnaire partagé `jobs`.
288
- """
289
- app.logger.info(f"Tâche {job_id}: Démarrage de {num_requests} requêtes vers {target_url}")
290
- completed_count: int = 0
291
- error_count: int = 0
292
- error_messages: List[Dict[str, Any]] = []
293
-
294
- # Note: L'initialisation de la tâche est maintenant faite dans /start avant le lancement du thread.
295
- # La partie ci-dessous est redondante mais laissée commentée pour info.
296
- # # Vérifier/Initialiser le statut (au cas où, bien que fait dans /start)
297
- # with jobs_lock:
298
- # if job_id not in jobs:
299
- # app.logger.warning(f"Tâche {job_id} non trouvée dans jobs au démarrage du thread. Initialisation.")
300
- # jobs[job_id] = {
301
- # 'status': STATUS_RUNNING, # On la met directement en running
302
- # 'total': num_requests,
303
- # 'completed_count': 0,
304
- # 'error_count': 0,
305
- # 'errors': []
306
- # }
307
- # else:
308
- # # Si elle existe déjà (normal), s'assurer qu'elle est en running
309
- # jobs[job_id]['status'] = STATUS_RUNNING
310
-
311
- # Mettre à jour le statut en 'running' une fois démarré
312
- with jobs_lock:
313
- if job_id in jobs:
314
- jobs[job_id]['status'] = STATUS_RUNNING
315
  else:
316
- # Cas très improbable si /start n'a pas fini avant que le thread ne check
317
- app.logger.error(f"Tâche {job_id} non trouvée au moment de passer en status RUNNING.")
318
- return # Arrêter le thread si la tâche n'existe pas
319
-
320
- for i in range(num_requests):
321
- success, error_msg = send_single_request(target_url, base_payload, job_id, i)
322
-
323
- # Mettre à jour la progression dans le dictionnaire partagé (section critique)
324
- with jobs_lock:
325
- # Vérifier si la tâche existe toujours (elle pourrait être supprimée?)
326
- if job_id not in jobs:
327
- app.logger.warning(f"Tâche {job_id} disparue pendant l'exécution. Arrêt.")
328
- break # Sortir de la boucle si la tâche n'est plus suivie
329
-
330
- # Incrémenter le compteur des requêtes traitées
331
- jobs[job_id]['completed_count'] += 1
332
- completed_count = jobs[job_id]['completed_count'] # Mettre à jour la variable locale aussi
 
 
 
 
 
 
 
 
 
 
 
333
 
334
- if not success:
335
- jobs[job_id]['error_count'] += 1
336
- error_count = jobs[job_id]['error_count'] # Mettre à jour la variable locale
337
- # Ajouter les détails de l'erreur (index basé sur 1 pour l'affichage)
338
- error_detail = {'index': i + 1, 'error': error_msg or "Erreur inconnue"}
339
- jobs[job_id]['errors'].append(error_detail)
340
- # Gardons seulement les X dernières erreurs pour éviter de saturer la mémoire
341
- max_errors_to_keep = 100
342
- jobs[job_id]['errors'] = jobs[job_id]['errors'][-max_errors_to_keep:]
 
 
 
343
 
344
- # Petite pause optionnelle pour ne pas surcharger la cible ou le réseau local
345
- # time.sleep(0.05) # 50ms pause
346
 
347
- # Marquer la tâche comme terminée une fois la boucle finie
348
- with jobs_lock:
349
- if job_id in jobs:
350
- # Déterminer le statut final basé sur les erreurs
351
- # Si toutes les requêtes ont échoué -> FAILED
352
- # Sinon -> COMPLETED (le nombre d'erreurs indique si c'était parfait ou non)
353
- final_status = STATUS_FAILED if error_count == num_requests and num_requests > 0 else STATUS_COMPLETED
354
- jobs[job_id]['status'] = final_status
355
- app.logger.info(f"Tâche {job_id}: Terminé. Statut final: {final_status}. {completed_count - error_count}/{num_requests} succès, {error_count} erreurs.")
356
- else:
357
- app.logger.warning(f"Tâche {job_id} non trouvée à la fin de l'exécution pour marquer comme terminée.")
358
 
 
 
 
 
359
 
360
- # --- Routes Flask ---
 
361
 
362
- @app.route('/', methods=['GET'])
363
- def index():
364
- """Affiche la page d'accueil avec le formulaire et la liste des tâches."""
365
- with jobs_lock:
366
- # Trie les tâches par clé (ID), en ordre inverse (plus récent d'abord si UUID approxime le temps)
367
- # Pour un tri chronologique fiable, il faudrait ajouter un timestamp lors de la création de la tâche.
368
- sorted_jobs = dict(sorted(jobs.items(), reverse=True))
369
- return render_template_string(HTML_INDEX, jobs_list=sorted_jobs)
370
 
371
- @app.route('/start', methods=['POST'])
372
- def start_requests():
373
- """
374
- Reçoit le nombre de requêtes depuis le formulaire,
375
- valide l'entrée, crée une nouvelle tâche et lance le thread d'arrière-plan.
376
- Redirige vers la page de statut de la nouvelle tâche.
377
- """
378
- num_requests_str = request.form.get('num_requests')
379
- num_requests: int = 0
380
 
381
  try:
382
- num_requests = int(num_requests_str)
383
- if num_requests <= 0:
384
- raise ValueError("Le nombre de requêtes doit être un entier positif.")
385
- if num_requests > 10000: # Limite de sécurité (optionnelle)
386
- raise ValueError("Le nombre de requêtes est limité à 10000.")
387
- except (TypeError, ValueError, AttributeError) as e:
388
- app.logger.warning(f"Tentative de démarrage échouée - nombre invalide: '{num_requests_str}' - Erreur: {e}")
389
- # Correction: Il faut re-passer la liste des jobs au template d'index en cas d'erreur
390
- with jobs_lock:
391
- sorted_jobs = dict(sorted(jobs.items(), reverse=True))
392
- return render_template_string(HTML_INDEX, error=f"Nombre de requêtes invalide : {e}", jobs_list=sorted_jobs), 400
393
 
394
- # Générer un ID de tâche unique (partie courte d'un UUID v4)
395
- job_id: str = str(uuid.uuid4())[:8]
 
 
 
 
396
 
397
- # Initialiser l'état de la tâche dans le dictionnaire partagé (section critique)
398
- with jobs_lock:
399
- jobs[job_id] = {
400
- 'status': STATUS_STARTING, # Statut initial avant que le thread ne démarre vraiment
401
- 'total': num_requests,
402
- 'completed_count': 0,
403
- 'error_count': 0,
404
- 'errors': []
405
- # Optionnel: Ajouter un timestamp de création
406
- # 'created_at': datetime.now(timezone.utc).isoformat()
407
- }
408
 
409
- # Créer et démarrer le thread pour exécuter la tâche en arrière-plan
410
- thread = threading.Thread(
411
- target=background_task,
412
- args=(job_id, num_requests, TARGET_URL, BASE_PAYLOAD),
413
- daemon=True # Permet au programme principal de quitter même si des threads sont encore en cours d'exécution
414
- )
415
- thread.start()
416
 
417
- app.logger.info(f"Nouvelle tâche {job_id} démarrée pour {num_requests} requêtes.")
418
- # Rediriger l'utilisateur vers la page de statut de cette nouvelle tâche
419
- return redirect(url_for('job_status', job_id=job_id))
420
 
421
- @app.route('/status/<job_id>', methods=['GET'])
422
- def job_status(job_id: str):
423
- """Affiche la page HTML de suivi pour une tâche spécifique (identifiée par job_id)."""
424
- with jobs_lock:
425
- # Vérifier si la tâche existe juste pour éviter d'afficher une page pour un ID invalide
426
- if job_id not in jobs:
427
- app.logger.warning(f"Tentative d'accès à la page de statut pour une tâche inexistante: {job_id}")
428
- return "Tâche non trouvée", 404
429
 
430
- # La page HTML contient le JavaScript qui appellera l'API '/api/status/<job_id>'
431
- # pour obtenir les données de progression dynamiquement.
432
- # On passe les constantes de statut au template pour que le JS puisse les utiliser
433
- return render_template_string(HTML_STATUS, job_id=job_id,
434
- STATUS_COMPLETED=STATUS_COMPLETED,
435
- STATUS_FAILED=STATUS_FAILED)
436
 
437
- @app.route('/api/status/<job_id>', methods=['GET'])
438
- def api_job_status(job_id: str):
439
- """
440
- Fournit l'état actuel d'une tâche spécifique au format JSON.
441
- Utilisé par le JavaScript de la page de statut pour les mises à jour.
442
- """
443
- with jobs_lock:
444
- # Obtenir les informations de la tâche. Utiliser .get() pour gérer le cas où l'ID n'existe pas.
445
- job_info = jobs.get(job_id)
446
 
447
- if job_info:
448
- # Renvoyer une copie profonde pour éviter toute modification concurrente pendant la sérialisation JSON
449
- # Bien que le lock aide, c'est une sécurité supplémentaire, surtout si les structures de données deviennent complexes.
450
- return jsonify(copy.deepcopy(job_info))
451
- else:
452
- # Si la tâche n'est pas trouvée, renvoyer une réponse JSON avec une erreur 404.
453
- app.logger.warning(f"API: Statut demandé pour tâche inexistante: {job_id}")
454
- return jsonify({"error": "Tâche non trouvée", "job_id": job_id}), 404
455
 
456
- # --- Démarrage de l'application ---
457
  if __name__ == '__main__':
458
- # Utiliser host='0.0.0.0' pour rendre l'application accessible
459
- # depuis d'autres machines sur le même réseau local.
460
- # ATTENTION : debug=True ne doit JAMAIS être utilisé dans un environnement de production.
461
- # Il expose des vulnérabilités de sécurité et affecte les performances.
462
- print("Démarrage du serveur Flask...")
463
- print(f"Accéder à l'application via http://localhost:5000 ou http://<votre_ip_locale>:5000")
464
- app.run(host='0.0.0.0', port=5000, debug=True) # Mettre debug=False pour la production
 
1
+ import os
2
+ import json
3
+ from textwrap import dedent
4
+ from flask import Flask, render_template, request, jsonify
5
+ from crewai import Agent, Crew, Process, Task
6
+ from crewai_tools import SerperDevTool
7
+ from langchain_google_genai import ChatGoogleGenerativeAI
8
+ from typing import List, Dict, Any
9
+
10
+ # Configuration (use .env in production)
11
+ os.environ["GEMINI_API_KEY"] = "AIzaSyCQpoVCdk7h7MvAQKZOUfcnkQYVkHmAKwI"
12
+ os.environ["SERPER_API_KEY"] = "9b90a274d9e704ff5b21c0367f9ae1161779b573"
13
+ gemini_api_key = os.environ.get("GEMINI_API_KEY")
14
+ serper_api_key = os.environ.get("SERPER_API_KEY")
15
+
16
+ if not gemini_api_key:
17
+ raise ValueError("Gemini API key missing.")
18
+ if not serper_api_key:
19
+ raise ValueError("Serper API key missing.")
20
+
21
+ llm = ChatGoogleGenerativeAI(
22
+ model="gemini-2.0-flash",
23
+ temperature=0.7,
24
+ google_api_key=gemini_api_key,
25
+ max_output_tokens=8000,
26
+ convert_system_message_to_human=True
27
+ )
28
+
29
+ search_tool = SerperDevTool()
30
+
31
+ researcher = Agent(
32
+ role='Chercheur de Sujets Académiques',
33
+ goal=dedent("""Find factual, precise information on {topic} using the web search tool.
34
+ Focus on key concepts, definitions, dates, and notable facts."""),
35
+ backstory=dedent("""A meticulous researcher with a developed critical mind.
36
+ Synthesizes information from multiple sources to extract the factual essence."""),
37
+ tools=[search_tool],
38
+ llm=llm,
39
+ verbose=True,
40
+ allow_delegation=False,
41
+ max_iter=5
42
+ )
43
+
44
+ quiz_creator = Agent(
45
+ role='Concepteur Pédagogique de Quiz',
46
+ goal=dedent("""Create an engaging multiple-choice quiz based on the factual information
47
+ provided by the Researcher on {topic}.
48
+ Formulate clear questions, plausible options, and identify the correct answer.
49
+ Include a concise explanation for each question."""),
50
+ backstory=dedent("""A specialist in pedagogical design who excels at transforming
51
+ raw information into effective evaluation tools."""),
52
+ llm=llm,
53
+ verbose=True,
54
+ allow_delegation=False,
55
+ max_iter=3
56
+ )
57
+
58
+ def extract_json_from_result(result_text: str) -> List[Dict[str, Any]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  try:
60
+ json_start = result_text.find('[')
61
+ json_end = result_text.rfind(']') + 1
62
+
63
+ if json_start != -1 and json_end != 0 and json_end > json_start:
64
+ json_str = result_text[json_start:json_end]
65
+ parsed_json = json.loads(json_str)
66
+ if isinstance(parsed_json, list):
67
+ for item in parsed_json:
68
+ if not all(k in item for k in ('question', 'options', 'correct_answer')):
69
+ raise ValueError("Invalid quiz element structure in JSON.")
70
+ return parsed_json
71
+ else:
72
+ raise ValueError("The extracted JSON is not a list.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  else:
74
+ try:
75
+ parsed_json = json.loads(result_text)
76
+ if isinstance(parsed_json, list):
77
+ for item in parsed_json:
78
+ if not all(k in item for k in ('question', 'options', 'correct_answer')):
79
+ raise ValueError("Invalid quiz element structure in JSON.")
80
+ return parsed_json
81
+ else:
82
+ raise ValueError("The JSON found is not a list.")
83
+ except json.JSONDecodeError:
84
+ raise ValueError("No valid JSON block (list of objects) found in the result.")
85
+
86
+ except json.JSONDecodeError as e:
87
+ raise ValueError(f"JSON decoding error: {str(e)}. Contenu reçu : '{result_text[:200]}...'")
88
+ except ValueError as e:
89
+ raise e
90
+
91
+ def research_task(topic: str, agent: Agent) -> Task:
92
+ return Task(
93
+ description=dedent(f"""Thorough research on '{topic}'.
94
+ Identify and compile key factual information: definitions, dates,
95
+ important figures, fundamental concepts, and significant events.
96
+ Structure the information clearly and concisely."""),
97
+ expected_output=dedent(f"""A synthetic report containing the most relevant information
98
+ on '{topic}', ready to be used to create a quiz.
99
+ Must include precise and verifiable facts."""),
100
+ agent=agent,
101
+ )
102
 
103
+ def quiz_creation_task(topic: str, agent: Agent, context_task: Task) -> Task:
104
+ return Task(
105
+ description=dedent(f"""Based STRICTLY on the information provided in the context
106
+ of the research on '{topic}', create a multiple-choice quiz.
107
+ Generate between 8 and 12 pertinent questions.
108
+ For each question, provide: 'question', 'options', 'correct_answer', and 'explanation'.
109
+ The output format MUST be a valid JSON list and NOTHING ELSE."""),
110
+ expected_output=dedent("""A valid JSON list, where each element is a quiz question
111
+ with the keys 'question', 'options', 'correct_answer', and 'explanation'."""),
112
+ agent=agent,
113
+ context=[context_task]
114
+ )
115
 
116
+ app = Flask(__name__)
117
+ app.secret_key = os.urandom(24)
118
 
119
+ @app.route('/')
120
+ def index():
121
+ return render_template('index.html')
 
 
 
 
 
 
 
 
122
 
123
+ @app.route('/generate', methods=['POST'])
124
+ def generate_quiz_endpoint():
125
+ if not request.is_json:
126
+ return jsonify({'error': 'Invalid request, JSON expected.'}), 400
127
 
128
+ data = request.get_json()
129
+ topic = data.get('topic')
130
 
131
+ if not topic or not isinstance(topic, str) or len(topic.strip()) == 0:
132
+ return jsonify({'error': 'The "topic" field is missing or invalid.'}), 400
 
 
 
 
 
 
133
 
134
+ topic = topic.strip()
 
 
 
 
 
 
 
 
135
 
136
  try:
137
+ task_research = research_task(topic=topic, agent=researcher)
138
+ task_quiz = quiz_creation_task(topic=topic, agent=quiz_creator, context_task=task_research)
 
 
 
 
 
 
 
 
 
139
 
140
+ quiz_crew = Crew(
141
+ agents=[researcher, quiz_creator],
142
+ tasks=[task_research, task_quiz],
143
+ process=Process.sequential,
144
+ verbose=2
145
+ )
146
 
147
+ crew_result = quiz_crew.kickoff(inputs={'topic': topic})
 
 
 
 
 
 
 
 
 
 
148
 
149
+ if not crew_result or not hasattr(quiz_crew, 'tasks_output') or not quiz_crew.tasks_output:
150
+ return jsonify({'error': 'Quiz generation failed (no crew output).'}), 500
 
 
 
 
 
151
 
152
+ last_task_output = quiz_crew.tasks_output[-1]
153
+ raw_output = last_task_output.raw
 
154
 
155
+ if not raw_output:
156
+ return jsonify({'error': 'Quiz generation failed (empty task output).'}), 500
 
 
 
 
 
 
157
 
158
+ try:
159
+ quiz_data = extract_json_from_result(raw_output)
160
+ return jsonify({'success': True, 'quiz': quiz_data})
 
 
 
161
 
162
+ except ValueError as json_error:
163
+ return jsonify({'error': f'Error during quiz finalization: {json_error}'}), 500
 
 
 
 
 
 
 
164
 
165
+ except Exception as e:
166
+ import traceback
167
+ traceback.print_exc()
168
+ return jsonify({'error': f'A server error occurred: {str(e)}'}), 500
 
 
 
 
169
 
 
170
  if __name__ == '__main__':
171
+ app.run(host='0.0.0.0', port=5000, debug=True)