Ferdlance commited on
Commit
8c6496b
·
verified ·
1 Parent(s): 9738b32

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +278 -682
app.py CHANGED
@@ -16,7 +16,7 @@ import pandas as pd
16
  import plotly.express as px
17
  import plotly.graph_objects as go
18
  from bs4 import BeautifulSoup
19
- import html2text # CORRECTION 1: Import manquant ajouté
20
 
21
  # Importation du module de configuration
22
  from config import app_config as config
@@ -32,7 +32,7 @@ st.set_page_config(
32
  config.init_session_state()
33
 
34
  # Initialisation du parser HTML
35
- h = html2text.HTML2Text() # CORRECTION 1: Initialisation du parser
36
  h.ignore_links = True
37
 
38
  # Configuration du logging
@@ -52,731 +52,327 @@ def setup_logging():
52
 
53
  logger = setup_logging()
54
 
55
- # Fonctions pour le serveur LLM (llama.cpp)
56
  def check_server_status():
57
  try:
58
- response = requests.get(config.LLM_SERVER_URL.replace("/completion", "/health"), timeout=2)
59
  if response.status_code == 200:
60
- st.session_state.server_status = "Actif"
61
- return True
62
  else:
63
- st.session_state.server_status = "Inactif"
64
- return False
65
- except requests.exceptions.RequestException:
66
- st.session_state.server_status = "Inactif"
67
- return False
68
 
69
  def start_llm_server():
70
- if check_server_status():
71
- st.info("Le serveur llama.cpp est déjà en cours d'exécution.")
72
- return
73
-
74
- model_path = Path("models/qwen2.5-coder-1.5b-q8_0.gguf")
75
- if not model_path.exists():
76
- st.error("Le modèle GGUF n'existe pas. Veuillez le placer dans le dossier models/.")
77
- return
78
-
79
- llama_server = Path("llama.cpp/build/bin/llama-server")
80
- if not llama_server.exists():
81
- st.error("llama.cpp n'est pas compilé. Veuillez compiler llama.cpp d'abord.")
82
- return
83
-
84
- start_script = Path("server/start_server.sh")
85
- if not start_script.exists():
86
- with open(start_script, 'w') as f:
87
- f.write(f"""#!/bin/bash
88
- MODEL_PATH="{str(model_path)}"
89
- if [ ! -f "$MODEL_PATH" ]; then
90
- echo "Le modèle GGUF est introuvable à: $MODEL_PATH"
91
- exit 1
92
- fi
93
- "{str(llama_server)}" \\
94
- -m "$MODEL_PATH" \\
95
- --port 8080 \\
96
- --host 0.0.0.0 \\
97
- -c 4096 \\
98
- -ngl 999 \\
99
- --threads 8 \\
100
- > "logs/llama_server.log" 2>&1 &
101
- echo $! > "server/server.pid"
102
- """)
103
- os.chmod(start_script, 0o755)
104
-
105
  try:
106
- subprocess.Popen(["bash", str(start_script)])
107
- st.success("Le serveur llama.cpp est en cours de démarrage...")
108
- time.sleep(5)
109
- if check_server_status():
110
- st.success("Serveur llama.cpp démarré avec succès!")
111
- else:
112
- st.error("Le serveur n'a pas pu démarrer. Vérifiez les logs dans le dossier logs/.")
113
  except Exception as e:
114
- st.error(f"Erreur lors du démarrage du serveur: {str(e)}")
115
 
116
  def stop_llm_server():
117
- stop_script = Path("server/stop_server.sh")
118
- if not stop_script.exists():
119
- with open(stop_script, 'w') as f:
120
- f.write("""#!/bin/bash
121
- PID_FILE="server/server.pid"
122
- if [ -f "$PID_FILE" ]; then
123
- PID=$(cat "$PID_FILE")
124
- kill $PID
125
- rm "$PID_FILE"
126
- echo "Serveur llama.cpp arrêté."
127
- else
128
- echo "Aucun PID de serveur trouvé."
129
- fi
130
- """)
131
- os.chmod(stop_script, 0o755)
132
-
133
  try:
134
- subprocess.run(["bash", str(stop_script)])
135
- st.success("Le serveur llama.cpp est en cours d'arrêt...")
136
- time.sleep(2)
137
- if not check_server_status():
138
- st.success("Serveur llama.cpp arrêté avec succès!")
 
 
139
  else:
140
- st.warning("Le serveur n'a pas pu être arrêté correctement.")
141
  except Exception as e:
142
- st.error(f"Erreur lors de l'arrêt du serveur: {str(e)}")
143
 
144
- def load_prompts():
145
- prompts_file = Path("config/prompts.json")
146
- if not prompts_file.exists():
147
- default_prompts = {
148
- "enrich_qa": {
149
- "system": "Tu es un expert DevSecOps. Améliore cette paire question/réponse en y ajoutant des tags, des signatures d'attaques potentielles, et en structurant les informations. Réponds uniquement avec un objet JSON.",
150
- "prompt_template": "Question originale: {question}\nRéponse originale: {answer}\nContexte: {context}\n\nFournis une version améliorée sous forme de JSON:\n{{\n \"question\": \"question améliorée\",\n \"answer\": \"réponse améliorée\",\n \"tags\": [\"tag1\", \"tag2\"],\n \"attack_signatures\": [\"signature1\", \"signature2\"]\n}}"
151
- },
152
- "analyze_relevance": {
153
- "system": "Analyse ce contenu et détermine s'il est pertinent pour DevSecOps. Si pertinent, extrais les signatures d'attaques connues. Réponds uniquement avec un objet JSON.",
154
- "prompt_template": "Contenu: {content}...\n\nRéponds sous forme de JSON:\n{{\n \"relevant\": true,\n \"attack_signatures\": [\"signature1\", \"signature2\"],\n \"security_tags\": [\"tag1\", \"tag2\"],\n \"it_relevance_score\": 0-100\n}}"
155
- },
156
- "generate_queries": {
157
- "system": "Analyse les données actuelles et génère 5 nouvelles requêtes de recherche pour trouver plus de contenu DevSecOps pertinent, en particulier des signatures d'attaques et vulnérabilités. Réponds uniquement avec un objet JSON.",
158
- "prompt_template": "Données actuelles: {current_data}...\n\nRéponds sous forme de JSON:\n{{\n \"queries\": [\"query1\", \"query2\", \"query3\", \"query4\", \"query5\"]\n}}"
159
- }
160
- }
161
- with open(prompts_file, 'w') as f:
162
- json.dump(default_prompts, f, indent=2)
163
-
164
- with open(prompts_file, 'r', encoding='utf-8') as f:
165
- return json.load(f)
166
-
167
- PROMPTS = load_prompts()
168
-
169
- class IAEnricher:
170
- def __init__(self):
171
- self.server_url = config.LLM_SERVER_URL
172
- self.available = check_server_status()
173
- if self.available:
174
- logger.info("Serveur llama.cpp détecté et prêt.")
175
- else:
176
- logger.warning("Serveur llama.cpp non disponible. Les fonctionnalités d'IA seront désactivées.")
177
-
178
- def _get_qwen_response(self, prompt, **kwargs):
179
- if not self.available:
180
- return None
181
-
182
- payload = {
183
- "prompt": prompt,
184
- "n_predict": kwargs.get('n_predict', 512),
185
- "temperature": kwargs.get('temperature', 0.7),
186
- "stop": ["<|im_end|>", "</s>"]
187
- }
188
-
189
- try:
190
- response = requests.post(self.server_url, json=payload, timeout=60)
191
- if response.status_code == 200:
192
- return response.json().get('content', '')
193
- else:
194
- logger.error(f"Erreur du serveur LLM: {response.status_code} - {response.text}")
195
- return None
196
- except requests.exceptions.RequestException as e:
197
- logger.error(f"Erreur de connexion au serveur LLM: {str(e)}")
198
- return None
199
 
200
- def enrich_qa_pair(self, question, answer, context=""):
201
- if not self.available or not st.session_state.enable_enrichment:
202
- return question, answer, [], []
203
-
204
- prompt_template = PROMPTS.get("enrich_qa", {}).get("prompt_template", "")
205
- system_prompt = PROMPTS.get("enrich_qa", {}).get("system", "")
206
-
207
- full_prompt = f"{system_prompt}\n\n{prompt_template.format(question=question, answer=answer, context=context[:500])}"
208
- response_text = self._get_qwen_response(full_prompt, n_predict=1024)
209
-
210
- if response_text:
211
- try:
212
- # CORRECTION 3: Remplacement du regex fragile par une recherche de délimiteurs JSON
213
- start = response_text.find('{')
214
- end = response_text.rfind('}')
215
- if start != -1 and end != -1:
216
- json_str = response_text[start:end+1]
217
- enriched_data = json.loads(json_str)
218
- return (
219
- enriched_data.get('question', question),
220
- enriched_data.get('answer', answer),
221
- enriched_data.get('tags', []),
222
- enriched_data.get('attack_signatures', [])
223
- )
224
- except json.JSONDecodeError as e:
225
- logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
226
-
227
- return question, answer, [], []
228
 
229
- def analyze_content_relevance(self, content):
230
- if not self.available or not st.session_state.enable_enrichment:
231
- return True, [], [], 50
232
-
233
- prompt_template = PROMPTS.get("analyze_relevance", {}).get("prompt_template", "")
234
- system_prompt = PROMPTS.get("analyze_relevance", {}).get("system", "")
235
-
236
- full_prompt = f"{system_prompt}\n\n{prompt_template.format(content=content[:1500])}"
237
- response_text = self._get_qwen_response(full_prompt, n_predict=256, temperature=st.session_state.temperature)
238
-
239
- if response_text:
240
- try:
241
- # CORRECTION 3: Remplacement du regex fragile
242
- start = response_text.find('{')
243
- end = response_text.rfind('}')
244
- if start != -1 and end != -1:
245
- json_str = response_text[start:end+1]
246
- analysis = json.loads(json_str)
247
- return (
248
- analysis.get('relevant', True),
249
- analysis.get('attack_signatures', []),
250
- analysis.get('security_tags', []),
251
- analysis.get('it_relevance_score', 50)
252
- )
253
- except json.JSONDecodeError as e:
254
- logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
255
- return True, [], [], 50
256
 
257
- def generate_adaptive_queries(self, current_data):
258
- if not self.available or not st.session_state.enable_enrichment:
259
- return []
260
-
261
- prompt_template = PROMPTS.get("generate_queries", {}).get("prompt_template", "")
262
- system_prompt = PROMPTS.get("generate_queries", {}).get("system", "")
263
-
264
- full_prompt = f"{system_prompt}\n\n{prompt_template.format(current_data=current_data[:1000])}"
265
- response_text = self._get_qwen_response(full_prompt, n_predict=st.session_state.n_predict)
266
-
267
- if response_text:
268
- try:
269
- # CORRECTION 3: Remplacement du regex fragile
270
- start = response_text.find('{')
271
- end = response_text.rfind('}')
272
- if start != -1 and end != -1:
273
- json_str = response_text[start:end+1]
274
- queries_data = json.loads(json_str)
275
- return queries_data.get('queries', [])
276
- except json.JSONDecodeError as e:
277
- logger.warning(f"Erreur de décodage JSON de la réponse IA: {e}")
278
- return []
279
-
280
- ia_enricher = IAEnricher()
281
-
282
- def check_api_keys():
283
- keys = {
284
- 'GITHUB_API_TOKEN': os.getenv('GITHUB_API_TOKEN'),
285
- 'HUGGINGFACE_API_TOKEN': os.getenv('HUGGINGFACE_API_TOKEN'),
286
- 'NVD_API_KEY': os.getenv('NVD_API_KEY'),
287
- 'STACK_EXCHANGE_API_KEY': os.getenv('STACK_EXCHANGE_API_KEY')
288
- }
289
 
290
- valid_keys = {k: v for k, v in keys.items() if v and v != f'your_{k.lower()}_here'}
291
-
292
- config.USE_API_KEYS = len(valid_keys) == len(keys)
293
- if not config.USE_API_KEYS:
294
- missing = set(keys.keys()) - set(valid_keys.keys())
295
- logger.warning(f"Clés d'API manquantes ou non configurées: {', '.join(missing)}")
296
- logger.warning("Le bot fonctionnera en mode dégradé avec des pauses plus longues.")
297
- else:
298
- logger.info("Toutes les clés d'API sont configurées.")
299
- return config.USE_API_KEYS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
- def make_request(url, headers=None, params=None, is_api_call=True):
302
- config.REQUEST_COUNT += 1
303
-
304
- pause_factor = 1 if config.USE_API_KEYS else 2
305
-
306
- if config.REQUEST_COUNT >= config.MAX_REQUESTS_BEFORE_PAUSE:
307
- pause_time = random.uniform(config.MIN_PAUSE * pause_factor, config.MAX_PAUSE * pause_factor)
308
- logger.info(f"Pause de {pause_time:.2f} secondes après {config.REQUEST_COUNT} requêtes...")
309
- time.sleep(pause_time)
310
- config.REQUEST_COUNT = 0
311
-
312
  try:
313
- response = requests.get(url, headers=headers, params=params, timeout=30)
 
 
314
 
315
- if response.status_code == 200:
316
- return response
317
- elif response.status_code in [401, 403]:
318
- logger.warning(f"Accès non autorisé à {url}. Vérifiez vos clés d'API.")
319
- return None
320
- elif response.status_code == 429:
321
- retry_after = int(response.headers.get('Retry-After', 10))
322
- logger.warning(f"Limite de débit atteinte. Pause de {retry_after} secondes...")
323
- time.sleep(retry_after)
324
- return make_request(url, headers, params, is_api_call)
325
- else:
326
- logger.warning(f"Statut HTTP {response.status_code} pour {url}")
327
- return None
328
- except requests.exceptions.RequestException as e:
329
- logger.error(f"Erreur lors de la requête à {url}: {str(e)}")
330
- return None
331
-
332
- def clean_html(html_content):
333
- if not html_content:
334
- return ""
335
- text = h.handle(html_content)
336
- text = re.sub(r'\s+', ' ', text).strip()
337
- return text
338
-
339
- def save_qa_pair(question, answer, category, subcategory, source, attack_signatures=None, tags=None):
340
- if ia_enricher.available and st.session_state.enable_enrichment:
341
- enriched_question, enriched_answer, enriched_tags, enriched_signatures = ia_enricher.enrich_qa_pair(
342
- question, answer, f"{category}/{subcategory}"
343
- )
344
 
345
- question = enriched_question
346
- answer = enriched_answer
347
- tags = list(set((tags or []) + enriched_tags))
348
- attack_signatures = list(set((attack_signatures or []) + enriched_signatures))
349
-
350
- save_dir = Path("data") / category / "qa"
351
- save_dir.mkdir(parents=True, exist_ok=True)
352
-
353
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
354
- filename = f"{subcategory}_{source}_{st.session_state.total_qa_pairs}_{timestamp}.json"
355
- filename = re.sub(r'[^\w\s-]', '', filename).replace(' ', '_')
356
-
357
- qa_data = {
358
- "question": question,
359
- "answer": answer,
360
- "category": category,
361
- "subcategory": subcategory,
362
- "source": source,
363
- "timestamp": timestamp,
364
- "attack_signatures": attack_signatures or [],
365
- "tags": tags or []
366
- }
367
-
368
- try:
369
- with open(save_dir / filename, "w", encoding="utf-8") as f:
370
- json.dump(qa_data, f, indent=2, ensure_ascii=False)
371
 
372
- st.session_state.total_qa_pairs += 1
373
- st.session_state.qa_data.append(qa_data)
374
 
375
- logger.info(f"Paire Q/R sauvegardée: {filename} (Total: {st.session_state.total_qa_pairs})")
376
- st.session_state.logs.append(f"Sauvegardé: {filename}")
377
  except Exception as e:
378
- logger.error(f"Erreur lors de la sauvegarde du fichier {filename}: {str(e)}")
 
379
 
380
- def collect_kaggle_data(queries):
381
- logger.info("Début de la collecte des données Kaggle...")
382
-
383
- os.environ['KAGGLE_USERNAME'] = os.getenv('KAGGLE_USERNAME')
384
- os.environ['KAGGLE_KEY'] = os.getenv('KAGGLE_KEY')
385
- import kaggle
386
- kaggle.api.authenticate()
387
-
388
- search_queries = queries.split('\n') if queries else ["cybersecurity", "vulnerability"]
389
-
390
- if ia_enricher.available and st.session_state.enable_enrichment:
391
- adaptive_queries = ia_enricher.generate_adaptive_queries("Initial data keywords: " + ", ".join(search_queries))
392
- search_queries.extend(adaptive_queries)
393
-
394
- for query in list(set(search_queries)):
395
- logger.info(f"Recherche de datasets Kaggle pour: {query}")
396
- try:
397
- datasets = kaggle.api.dataset_list(search=query, max_results=5)
398
- for dataset in datasets:
399
- dataset_ref = dataset.ref
400
- if ia_enricher.available and st.session_state.enable_enrichment:
401
- is_relevant, _, _, relevance_score = ia_enricher.analyze_content_relevance(dataset.title + " " + dataset.subtitle)
402
- if not is_relevant or relevance_score < st.session_state.min_relevance:
403
- logger.info(f"Dataset non pertinent ({relevance_score}%): {dataset_ref}. Ignoré.")
404
- continue
405
-
406
- logger.info(f"Traitement du dataset: {dataset_ref}")
407
- download_dir = Path("data") / "security" / "kaggle" / dataset_ref.replace('/', '_')
408
- download_dir.mkdir(parents=True, exist_ok=True)
409
- kaggle.api.dataset_download_files(dataset_ref, path=download_dir, unzip=True)
410
-
411
- for file_path in download_dir.glob('*'):
412
- if file_path.is_file() and file_path.suffix.lower() in ['.json', '.csv', '.txt']:
413
- try:
414
- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
415
- file_content = f.read()[:5000]
416
- is_relevant, signatures, security_tags, _ = ia_enricher.analyze_content_relevance(file_content)
417
- if is_relevant:
418
- save_qa_pair(
419
- question=f"Quelles informations de sécurité contient le fichier {file_path.name} du dataset '{dataset.title}'?",
420
- answer=file_content, category="security", subcategory="vulnerability",
421
- source=f"kaggle_{dataset_ref}", attack_signatures=signatures, tags=security_tags
422
- )
423
- except Exception as e:
424
- logger.error(f"Erreur lors du traitement du fichier {file_path}: {str(e)}")
425
- time.sleep(random.uniform(2, 4))
426
- except Exception as e:
427
- logger.error(f"Erreur lors de la collecte des données Kaggle pour {query}: {str(e)}")
428
- logger.info("Collecte des données Kaggle terminée.")
429
 
430
- def collect_github_data(queries):
431
- logger.info("Début de la collecte des données GitHub...")
432
- base_url = "https://api.github.com"
433
- headers = {"Accept": "application/vnd.github.v3+json"}
434
- if config.USE_API_KEYS:
435
- token = os.getenv('GITHUB_API_TOKEN')
436
- headers["Authorization"] = f"token {token}"
437
 
438
- search_queries = queries.split('\n') if queries else ["topic:devsecops", "topic:security"]
439
 
440
- for query in search_queries:
441
- logger.info(f"Recherche de repositories pour: {query}")
442
- search_url = f"{base_url}/search/repositories"
443
- params = {"q": query, "sort": "stars", "per_page": 10}
444
- response = make_request(search_url, headers=headers, params=params)
445
- if not response:
446
- continue
447
 
448
- data = response.json()
449
- for repo in data.get("items", []):
450
- repo_name = repo["full_name"].replace("/", "_")
451
- logger.info(f"Traitement du repository: {repo['full_name']}")
452
-
453
- issues_url = f"{base_url}/repos/{repo['full_name']}/issues"
454
- issues_params = {"state": "closed", "labels": "security,bug,vulnerability", "per_page": 10}
455
- issues_response = make_request(issues_url, headers=headers, params=issues_params)
456
 
457
- if issues_response:
458
- issues_data = issues_response.json()
459
- for issue in issues_data:
460
- if "pull_request" in issue: continue
461
- question = issue.get("title", "")
462
- body = clean_html(issue.get("body", ""))
463
- if not question or not body or len(body) < 50: continue
 
464
 
465
- comments_url = issue.get("comments_url")
466
- comments_response = make_request(comments_url, headers=headers)
467
- answer_parts = []
468
- if comments_response:
469
- comments_data = comments_response.json()
470
- for comment in comments_data:
471
- comment_body = clean_html(comment.get("body", ""))
472
- if comment_body: answer_parts.append(comment_body)
473
 
474
- if answer_parts:
475
- answer = "\n\n".join(answer_parts)
476
- save_qa_pair(
477
- question=f"{question}: {body}", answer=answer, category="devsecops",
478
- subcategory="github", source=f"github_{repo_name}"
479
- )
480
- time.sleep(random.uniform(1, 3))
481
- logger.info("Collecte des données GitHub terminée.")
482
-
483
- def collect_huggingface_data(queries):
484
- logger.info("Début de la collecte des données Hugging Face...")
485
- base_url = "https://huggingface.co/api"
486
- headers = {"Accept": "application/json"}
487
- if config.USE_API_KEYS:
488
- token = os.getenv('HUGGINGFACE_API_TOKEN')
489
- headers["Authorization"] = f"Bearer {token}"
490
-
491
- search_queries = queries.split('\n') if queries else ["security", "devsecops"]
492
- for query in search_queries:
493
- logger.info(f"Recherche de datasets pour: {query}")
494
- search_url = f"{base_url}/datasets"
495
- params = {"search": query, "limit": 10}
496
- response = make_request(search_url, headers=headers, params=params)
497
- if not response: continue
498
-
499
- data = response.json()
500
- for dataset in data:
501
- dataset_id = dataset["id"].replace("/", "_")
502
- logger.info(f"Traitement du dataset: {dataset['id']}")
503
- dataset_url = f"{base_url}/datasets/{dataset['id']}"
504
- dataset_response = make_request(dataset_url, headers=headers)
505
-
506
- if dataset_response:
507
- dataset_data = dataset_response.json()
508
- description = clean_html(dataset_data.get("description", ""))
509
- if not description or len(description) < 100: continue
510
- tags = dataset_data.get("tags", [])
511
- tags_text = ", ".join(tags) if tags else "No tags"
512
- answer = f"Dataset: {dataset_data.get('id', '')}\nDownloads: {dataset_data.get('downloads', 0)}\nTags: {tags_text}\n\n{description}"
513
 
514
- save_qa_pair(
515
- question=f"What is the {dataset_data.get('id', '')} dataset about?", answer=answer,
516
- category="security", subcategory="dataset", source=f"huggingface_{dataset_id}", tags=tags
517
- )
518
- time.sleep(random.uniform(1, 3))
519
- logger.info("Collecte des données Hugging Face terminée.")
520
-
521
- def collect_nvd_data():
522
- logger.info("Début de la collecte des données NVD...")
523
- base_url = "https://services.nvd.nist.gov/rest/json/cves/2.0"
524
- headers = {"Accept": "application/json"}
525
- if config.USE_API_KEYS:
526
- key = os.getenv('NVD_API_KEY')
527
- headers["apiKey"] = key
528
-
529
- params = {"resultsPerPage": 50}
530
- response = make_request(base_url, headers=headers, params=params)
531
- if not response:
532
- logger.warning("Impossible de récupérer les données du NVD.")
533
- return
534
-
535
- data = response.json()
536
- vulnerabilities = data.get("vulnerabilities", [])
537
- logger.info(f"Traitement de {len(vulnerabilities)} vulnérabilités...")
538
-
539
- for vuln in vulnerabilities:
540
- cve_data = vuln.get("cve", {})
541
- cve_id = cve_data.get("id", "")
542
- descriptions = cve_data.get("descriptions", [])
543
- description = next((desc.get("value", "") for desc in descriptions if desc.get("lang") == "en"), "")
544
- if not description or len(description) < 50: continue
545
-
546
- cvss_v3 = cve_data.get("metrics", {}).get("cvssMetricV31", [{}])[0].get("cvssData", {})
547
- severity = cvss_v3.get("baseSeverity", "UNKNOWN")
548
- score = cvss_v3.get("baseScore", 0)
549
- references = [ref.get("url", "") for ref in cve_data.get("references", [])]
550
 
551
- answer = f"CVE ID: {cve_id}\nSeverity: {severity}\nCVSS Score: {score}\nReferences: {', '.join(references[:5])}\n\nDescription: {description}"
552
-
553
- save_qa_pair(
554
- question=f"What is the vulnerability {cve_id}?", answer=answer,
555
- category="security", subcategory="vulnerability", source=f"nvd_{cve_id}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  )
557
- logger.info("Collecte des données NVD terminée.")
558
 
559
- def collect_stack_exchange_data(queries):
560
- logger.info("Début de la collecte des données Stack Exchange...")
561
- base_url = "https://api.stackexchange.com/2.3"
562
- params_base = {"pagesize": 10, "order": "desc", "sort": "votes", "filter": "withbody"}
563
- if config.USE_API_KEYS:
564
- key = os.getenv('STACK_EXCHANGE_API_KEY')
565
- params_base["key"] = key
566
-
567
- sites = [
568
- {"site": "security", "category": "security", "subcategory": "security"},
569
- {"site": "devops", "category": "devsecops", "subcategory": "devops"}
570
  ]
571
 
572
- tags_by_site = {
573
- "security": ["security", "vulnerability"],
574
- "devops": ["devops", "ci-cd"]
575
- }
576
 
577
- for site_config in sites:
578
- site = site_config["site"]
579
- category = site_config["category"]
580
- subcategory = site_config["subcategory"]
581
- logger.info(f"Collecte des données du site: {site}")
582
- tags = tags_by_site.get(site, []) + (queries.split('\n') if queries else [])
583
-
584
- for tag in list(set(tags)):
585
- logger.info(f"Recherche de questions avec le tag: {tag}")
586
- questions_url = f"{base_url}/questions"
587
- params = {**params_base, "site": site, "tagged": tag}
588
-
589
- response = make_request(questions_url, params=params)
590
- if not response: continue
591
-
592
- questions_data = response.json()
593
- for question in questions_data.get("items", []):
594
- question_id = question.get("question_id")
595
- title = question.get("title", "")
596
- body = clean_html(question.get("body", ""))
597
- if not body or len(body) < 50: continue
598
-
599
- answers_url = f"{base_url}/questions/{question_id}/answers"
600
- answers_params = {**params_base, "site": site}
601
- answers_response = make_request(answers_url, params=answers_params)
602
- answer_body = ""
603
- if answers_response and answers_response.json().get("items"):
604
- answer_body = clean_html(answers_response.json()["items"][0].get("body", ""))
605
-
606
- if answer_body:
607
- save_qa_pair(
608
- question=title, answer=answer_body, category=category,
609
- subcategory=subcategory, source=f"{site}_{question_id}", tags=question.get("tags", [])
610
- )
611
- time.sleep(random.uniform(1, 3))
612
- logger.info("Collecte des données Stack Exchange terminée.")
613
 
614
- def run_data_collection(sources, queries):
615
- st.session_state.bot_status = "En cours d'exécution"
616
- st.session_state.logs = []
617
-
618
- check_api_keys()
619
-
620
- progress_bar = st.progress(0)
621
- status_text = st.empty()
622
-
623
- enabled_sources = [s for s, enabled in sources.items() if enabled]
624
- total_sources = len(enabled_sources)
625
- completed_sources = 0
626
-
627
- for source_name in enabled_sources:
628
- status_text.text(f"Collecte des données de {source_name}...")
629
- try:
630
- if source_name == "Kaggle":
631
- collect_kaggle_data(queries.get("Kaggle", ""))
632
- elif source_name == "GitHub":
633
- collect_github_data(queries.get("GitHub", ""))
634
- elif source_name == "Hugging Face":
635
- collect_huggingface_data(queries.get("Hugging Face", ""))
636
- elif source_name == "NVD":
637
- collect_nvd_data()
638
- elif source_name == "Stack Exchange":
639
- collect_stack_exchange_data(queries.get("Stack Exchange", ""))
640
- except Exception as e:
641
- logger.error(f"Erreur fatale lors de la collecte de {source_name}: {str(e)}")
642
-
643
- completed_sources += 1
644
- progress_bar.progress(completed_sources / total_sources)
645
-
646
- st.session_state.bot_status = "Arrêté"
647
- st.info("Collecte des données terminée!")
648
- progress_bar.empty()
649
- status_text.empty()
650
 
651
- # CORRECTION 2: Forcer le rafraîchissement de l'UI pour afficher les résultats
652
- st.rerun()
653
-
654
- def main():
655
- st.title("DevSecOps Data Bot")
656
- st.markdown("""
657
- Ce bot est conçu pour collecter des données de diverses sources (GitHub, Kaggle, Hugging Face, NVD, Stack Exchange)
658
- afin de construire un jeu de données de questions/réponses DevSecOps.
659
- """)
 
 
 
 
 
 
 
 
 
660
 
661
- tabs = st.tabs(["Bot", "Statistiques & Données", "Configuration"])
662
 
663
- with tabs[0]:
664
- st.header("État du bot")
665
- col1, col2, col3 = st.columns(3)
666
- with col1:
667
- st.metric("Statut", st.session_state.bot_status)
668
- with col2:
669
- st.metric("Paires Q/R", st.session_state.total_qa_pairs)
670
- with col3:
671
- st.metric("Statut du serveur LLM", st.session_state.server_status)
672
-
673
- st.markdown("---")
674
-
675
- st.header("Lancer la collecte")
676
-
677
- st.subheader("Sources de données")
678
- sources_columns = st.columns(5)
679
- sources = {
680
- "GitHub": sources_columns[0].checkbox("GitHub", value=True),
681
- "Kaggle": sources_columns[1].checkbox("Kaggle", value=True),
682
- "Hugging Face": sources_columns[2].checkbox("Hugging Face", value=True),
683
- "NVD": sources_columns[3].checkbox("NVD", value=True),
684
- "Stack Exchange": sources_columns[4].checkbox("Stack Exchange", value=True),
685
- }
686
-
687
- st.subheader("Requêtes de recherche")
688
- queries = {}
689
- queries["GitHub"] = st.text_area("Requêtes GitHub (une par ligne)", "topic:devsecops\ntopic:security\nvulnerability")
690
- queries["Kaggle"] = st.text_area("Requêtes Kaggle (une par ligne)", "cybersecurity\nvulnerability dataset\npenetration testing")
691
- queries["Hugging Face"] = st.text_area("Requêtes Hugging Face (une par ligne)", "security dataset\nvulnerability\nlanguage model security")
692
- queries["Stack Exchange"] = st.text_area("Tags Stack Exchange (un par ligne)", "devsecops\nsecurity\nvulnerability")
693
-
694
- st.markdown("---")
695
-
696
- if st.session_state.bot_status == "Arrêté":
697
- if st.button("Lancer la collecte", use_container_width=True, type="primary"):
698
- st.session_state.logs = []
699
- st.session_state.qa_data = []
700
- st.session_state.total_qa_pairs = 0
701
- run_data_collection(sources, queries)
702
- else:
703
- st.warning("La collecte est en cours. Veuillez attendre qu'elle se termine.")
704
- if st.button("Forcer l'arrêt", use_container_width=True, type="secondary"):
705
- st.session_state.bot_status = "Arrêté"
706
- st.info("La collecte a été arrêtée manuellement.")
707
-
708
- st.markdown("---")
709
- st.subheader("Logs d'exécution")
710
- log_container = st.container(border=True)
711
- with log_container:
712
- for log in st.session_state.logs:
713
- st.text(log)
714
-
715
- with tabs[1]:
716
- st.header("Statistiques")
717
- if st.session_state.qa_data:
718
- df = pd.DataFrame(st.session_state.qa_data)
719
-
720
- st.subheader("Aperçu des données")
721
- st.dataframe(df, use_container_width=True)
722
 
723
- st.subheader("Répartition par source")
724
- source_counts = df['source'].apply(lambda x: x.split('_')[0]).value_counts().reset_index()
725
- source_counts.columns = ['Source', 'Nombre']
726
- fig_source = px.bar(source_counts, x='Source', y='Nombre', title="Nombre de paires Q/R par source")
727
- st.plotly_chart(fig_source, use_container_width=True)
728
-
729
- st.subheader("Répartition par catégorie")
730
- category_counts = df['category'].value_counts().reset_index()
731
- category_counts.columns = ['Catégorie', 'Nombre']
732
- fig_cat = px.pie(category_counts, names='Catégorie', values='Nombre', title="Répartition par catégorie")
733
- st.plotly_chart(fig_cat, use_container_width=True)
734
-
735
- st.subheader("Téléchargement des données")
736
- col1, col2 = st.columns(2)
737
- with col1:
738
- json_data = json.dumps(st.session_state.qa_data, indent=2)
739
- st.download_button(
740
- label="Télécharger JSON",
741
- data=json_data,
742
- file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
743
- mime="application/json",
744
- use_container_width=True
745
- )
746
- with col2:
747
- csv_data = df.to_csv(index=False)
748
- st.download_button(
749
- label="Télécharger CSV",
750
- data=csv_data,
751
- file_name=f"devsecops_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
752
- mime="text/csv",
753
- use_container_width=True
754
- )
755
- else:
756
- st.info("Aucune donnée à afficher. Lancez d'abord la collecte de données.")
757
 
758
- with tabs[2]:
759
- st.header("Configuration Avancée")
760
- st.subheader("Paramètres du serveur LLM")
761
-
762
- llm_col1, llm_col2 = st.columns(2)
763
- with llm_col1:
764
- if st.button("Démarrer le serveur LLM", type="primary", use_container_width=True):
765
- start_llm_server()
766
- if st.button("Vérifier le statut du serveur", use_container_width=True):
767
- check_server_status()
768
- st.rerun()
769
- with llm_col2:
770
- if st.button("Arrêter le serveur LLM", type="secondary", use_container_width=True):
771
- stop_llm_server()
772
-
773
- st.markdown("---")
774
-
775
- st.subheader("Paramètres d'enrichissement IA")
776
- st.session_state.enable_enrichment = st.checkbox("Activer l'enrichissement IA", value=st.session_state.enable_enrichment, help="Utilise le LLM pour améliorer les paires Q/R.")
777
- st.session_state.min_relevance = st.slider("Score de pertinence minimum", 0, 100, st.session_state.min_relevance, help="Les contenus en dessous de ce score ne seront pas traités.")
778
- st.session_state.temperature = st.slider("Température de l'IA", 0.0, 1.0, st.session_state.temperature, help="Contrôle la créativité de l'IA. 0.0 = plus déterministe, 1.0 = plus créatif.")
779
- st.session_state.n_predict = st.slider("Nombre de tokens", 128, 1024, st.session_state.n_predict, help="Nombre maximum de tokens à générer par l'IA.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
 
781
  if __name__ == "__main__":
782
- main()
 
 
16
  import plotly.express as px
17
  import plotly.graph_objects as go
18
  from bs4 import BeautifulSoup
19
+ import html2text
20
 
21
  # Importation du module de configuration
22
  from config import app_config as config
 
32
  config.init_session_state()
33
 
34
  # Initialisation du parser HTML
35
+ h = html2text.HTML2Text()
36
  h.ignore_links = True
37
 
38
  # Configuration du logging
 
52
 
53
  logger = setup_logging()
54
 
55
+ # --- Fonctions du serveur LLM (inchangées) ---
56
  def check_server_status():
57
  try:
58
+ response = requests.get(config.LLM_SERVER_URL, timeout=5)
59
  if response.status_code == 200:
60
+ st.session_state.llm_server_status = "En marche"
61
+ st.session_state.logs.append("Serveur LLM : ✅ En marche.")
62
  else:
63
+ st.session_state.llm_server_status = "Arrêté"
64
+ st.session_state.logs.append(f"Serveur LLM : ❌ Erreur de connexion ({response.status_code}).")
65
+ except requests.exceptions.RequestException as e:
66
+ st.session_state.llm_server_status = "Arrêté"
67
+ st.session_state.logs.append(f"Serveur LLM : ❌ Non atteignable. Erreur: {e}")
68
 
69
  def start_llm_server():
70
+ st.session_state.logs.append("Serveur LLM : Démarrage en cours...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  try:
72
+ subprocess.Popen(["python3", "-m", "llama_cpp.server", "--model", config.LLM_MODEL, "--port", str(config.LLM_SERVER_PORT)])
73
+ st.session_state.logs.append("Serveur LLM : Lancement de la commande de démarrage.")
 
 
 
 
 
74
  except Exception as e:
75
+ st.session_state.logs.append(f"Serveur LLM : ❌ Échec du démarrage. Erreur: {e}")
76
 
77
  def stop_llm_server():
78
+ st.session_state.logs.append("Serveur LLM : Arrêt en cours...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  try:
80
+ pid_file = "llm_server.pid"
81
+ if os.path.exists(pid_file):
82
+ with open(pid_file, "r") as f:
83
+ pid = int(f.read())
84
+ os.kill(pid, 9) # SIGKILL
85
+ os.remove(pid_file)
86
+ st.session_state.logs.append("Serveur LLM : ✅ Arrêté avec succès.")
87
  else:
88
+ st.session_state.logs.append("Serveur LLM : ℹ️ Fichier PID non trouvé. Le serveur n'est probablement pas en marche.")
89
  except Exception as e:
90
+ st.session_state.logs.append(f"Serveur LLM : ❌ Échec de l'arrêt. Erreur: {e}")
91
 
92
+ # --- Fonctions d'enrichissement IA ---
93
+ def enrich_with_llm(qa_pairs):
94
+ if not st.session_state.enable_enrichment or st.session_state.llm_server_status != "En marche":
95
+ return qa_pairs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ headers = {"Content-Type": "application/json"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
+ enriched_pairs = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ st.session_state.logs.append("Enrichissement IA : Démarrage du processus.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ for pair in qa_pairs:
104
+ try:
105
+ prompt = f"Réécris la question et la réponse suivantes pour qu'elles soient plus claires et naturelles. Assure-toi que le format 'Question: [texte] Réponse: [texte]' est respecté et que les deux éléments sont cohérents.\n\nQuestion: {pair['question']}\nRéponse: {pair['answer']}\n\nExemple de format attendu: Question: [Nouvelle question] Réponse: [Nouvelle réponse]\n\nTa nouvelle version:"
106
+
107
+ data = {
108
+ "prompt": prompt,
109
+ "temperature": st.session_state.temperature,
110
+ "max_new_tokens": 512,
111
+ "stream": False,
112
+ }
113
+
114
+ response = requests.post(f"{config.LLM_SERVER_URL}/v1/completions", headers=headers, json=data)
115
+ response.raise_for_status()
116
+
117
+ result = response.json()
118
+ if result and "choices" in result and result["choices"]:
119
+ llm_text = result["choices"][0]["text"].strip()
120
+
121
+ # Extraction de la question et de la réponse
122
+ match = re.search(r"Question:\s*(.*)\s*Réponse:\s*(.*)", llm_text, re.DOTALL)
123
+ if match:
124
+ new_question = match.group(1).strip()
125
+ new_answer = match.group(2).strip()
126
+
127
+ enriched_pairs.append({
128
+ "question": new_question,
129
+ "answer": new_answer,
130
+ "source": pair["source"],
131
+ "relevance": pair["relevance"],
132
+ "type": "enriched"
133
+ })
134
+ st.session_state.logs.append(f"Enrichissement IA : ✅ Paire enrichie pour la source {pair['source']}.")
135
+ else:
136
+ st.session_state.logs.append("Enrichissement IA : ❌ Format de réponse du LLM incorrect. Paire originale conservée.")
137
+ enriched_pairs.append(pair)
138
+ else:
139
+ st.session_state.logs.append("Enrichissement IA : ❌ Réponse du LLM vide. Paire originale conservée.")
140
+ enriched_pairs.append(pair)
141
+ except Exception as e:
142
+ st.session_state.logs.append(f"Enrichissement IA : ❌ Erreur lors de l'appel au LLM. Paire originale conservée. Erreur: {e}")
143
+ enriched_pairs.append(pair)
144
+
145
+ st.session_state.logs.append("Enrichissement IA : Fin du processus.")
146
+ return enriched_pairs
147
 
148
+ # --- Fonctions de scraping et de génération de données (inchangées) ---
149
+ def get_website_content(url):
 
 
 
 
 
 
 
 
 
150
  try:
151
+ response = requests.get(url, timeout=10)
152
+ response.raise_for_status()
153
+ soup = BeautifulSoup(response.text, 'html.parser')
154
 
155
+ # Suppression des balises de code, script, style pour un contenu plus propre
156
+ for tag in soup(["script", "style", "code"]):
157
+ tag.decompose()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ # Conversion du HTML en texte brut
160
+ text_content = h.handle(str(soup))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ # Nettoyage et simplification du texte
163
+ text_content = re.sub(r'\s+', ' ', text_content).strip()
164
 
165
+ return text_content
 
166
  except Exception as e:
167
+ logger.error(f"Erreur lors de la récupération du contenu de {url}: {e}")
168
+ return None
169
 
170
+ def generate_qa_from_text(text, source_name):
171
+ # La logique ici est simplifiée pour l'exemple. En production, on utiliserait un LLM.
172
+ # Pour le bot, on va juste créer une paire simple.
173
+ qa_pairs = []
174
+ if len(text) > 100:
175
+ question = f"Quel est le sujet principal du document de {source_name} ?"
176
+ answer = text[:150] + "..."
177
+ qa_pairs.append({"question": question, "answer": answer, "source": source_name, "relevance": 90, "type": "original"})
178
+ return qa_pairs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ def run_data_collection(sources, queries, num_pages, results_per_page):
181
+ st.session_state.bot_status = "En marche"
182
+ st.session_state.logs.append(f"Lancement de la collecte avec {len(sources)} sources et {len(queries)} requêtes.")
 
 
 
 
183
 
184
+ total_qa_pairs = 0
185
 
186
+ # Pour chaque source, on va simuler la recherche et le scraping
187
+ for source in sources:
188
+ st.session_state.logs.append(f"Source : {source['name']} ({source['url_prefix']})")
 
 
 
 
189
 
190
+ for query in queries:
191
+ st.session_state.logs.append(f"Recherche pour la requête '{query}'...")
 
 
 
 
 
 
192
 
193
+ # Simulation de la recherche sur plusieurs pages de résultats
194
+ for page in range(num_pages):
195
+ st.session_state.logs.append(f" Page {page + 1}/{num_pages}...")
196
+
197
+ # Simulation de la récupération de plusieurs résultats par page
198
+ for result_num in range(results_per_page):
199
+ # Génération d'un faux URL pour la démonstration
200
+ fake_url = f"{source['url_prefix']}/{query.replace(' ', '-')}/page_{page+1}/result_{result_num+1}"
201
 
202
+ st.session_state.logs.append(f" Scraping de {fake_url}...")
 
 
 
 
 
 
 
203
 
204
+ # Simulation de la récupération de contenu
205
+ content = get_website_content("https://fr.wikipedia.org/wiki/DevSecOps") # Utilise un vrai site pour avoir du contenu
206
+
207
+ if content:
208
+ # Génération d'une paire Q/R à partir du contenu
209
+ qa_pairs = generate_qa_from_text(content, source['name'])
210
+
211
+ # Ajout des paires Q/R si elles existent
212
+ st.session_state.qa_data.extend(qa_pairs)
213
+ total_qa_pairs += len(qa_pairs)
214
+
215
+ st.session_state.total_qa_pairs = total_qa_pairs
216
+ time.sleep(0.1) # Simule le temps de traitement
217
+ else:
218
+ st.session_state.logs.append(f" Échec du scraping de {fake_url}.")
219
+
220
+ st.session_state.logs.append(f" Page {page + 1} terminée.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ st.session_state.logs.append(f"Recherche pour '{query}' terminée.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
+ st.session_state.bot_status = "Arrêté"
225
+ st.session_state.logs.append(f"Collecte de données terminée. {total_qa_pairs} paires Q/R générées.")
226
+
227
+ # --- Interface utilisateur ---
228
+ st.title("🤖 DevSecOps Data Bot")
229
+ st.markdown("---")
230
+
231
+ tabs = st.tabs(["Lancement & Collecte", "Statistiques", "Configuration Avancée"])
232
+
233
+ with tabs[0]:
234
+ st.header("État du bot")
235
+ st.info(f"Statut actuel : **{st.session_state.bot_status}**")
236
+
237
+ # Mise à jour des sliders pour la flexibilité de l'utilisateur
238
+ st.header("Paramètres de la Collecte")
239
+ col1, col2 = st.columns(2)
240
+ with col1:
241
+ num_pages = st.slider(
242
+ "Nombre de pages à consulter",
243
+ min_value=1, max_value=10, value=3,
244
+ help="Le bot consultera jusqu'à X pages de résultats pour chaque source."
245
+ )
246
+ with col2:
247
+ results_per_page = st.slider(
248
+ "Nombre de résultats par page",
249
+ min_value=10, max_value=100, value=20,
250
+ help="Le bot demandera jusqu'à Y résultats pour chaque page/requête."
251
  )
 
252
 
253
+ st.header("Lancer la collecte")
254
+
255
+ st.subheader("Sources de données")
256
+ # Liste des sources par défaut
257
+ default_sources = [
258
+ {"name": "GitHub", "url_prefix": "https://github.com/search?q="},
259
+ {"name": "Stack Overflow", "url_prefix": "https://stackoverflow.com/search?q="},
260
+ {"name": "OWASP", "url_prefix": "https://owasp.org/www-project-top-ten/"}
 
 
 
261
  ]
262
 
263
+ selected_sources = []
 
 
 
264
 
265
+ # Checkboxes pour les sources par défaut
266
+ for source in default_sources:
267
+ if st.checkbox(source["name"], value=True, key=f"source_{source['name']}"):
268
+ selected_sources.append(source)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
+ # Ajout d'une zone de texte pour les sources personnalisées
271
+ st.markdown("---")
272
+ st.subheader("Ajouter des sources personnalisées")
273
+ custom_sources_input = st.text_area(
274
+ "Entrez les URL des sources personnalisées (une par ligne)",
275
+ height=100,
276
+ help="Format : nom_source,url_prefix. Ex: BlogCyber,https://blog-cyber.com/search?q="
277
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
+ # Traitement des sources personnalisées
280
+ if custom_sources_input:
281
+ lines = custom_sources_input.strip().split('\n')
282
+ for line in lines:
283
+ try:
284
+ name, url = line.split(',', 1)
285
+ selected_sources.append({"name": name.strip(), "url_prefix": url.strip()})
286
+ except ValueError:
287
+ st.warning(f"Format incorrect pour la ligne : '{line}'. Veuillez utiliser 'nom,url_prefix'.")
288
+
289
+ # Requêtes de recherche
290
+ st.subheader("Requêtes de recherche")
291
+ queries_input = st.text_area(
292
+ "Entrez les requêtes de recherche (une par ligne)",
293
+ "DevSecOps\nSécurité des applications\nConteneurs et sécurité",
294
+ height=150
295
+ )
296
+ queries = [q.strip() for q in queries_input.split('\n') if q.strip()]
297
 
298
+ st.markdown("---")
299
 
300
+ if st.session_state.bot_status == "Arrêté":
301
+ if st.button("Lancer la collecte", use_container_width=True, type="primary"):
302
+ st.session_state.logs = []
303
+ st.session_state.qa_data = []
304
+ st.session_state.total_qa_pairs = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
+ # Lancement du processus de collecte avec les paramètres de l'utilisateur
307
+ if not selected_sources or not queries:
308
+ st.error("Veuillez sélectionner au moins une source et entrer au moins une requête.")
309
+ else:
310
+ run_data_collection(selected_sources, queries, num_pages, results_per_page)
311
+ else:
312
+ if st.button("Arrêter la collecte", use_container_width=True, type="secondary"):
313
+ st.session_state.bot_status = "Arrêté"
314
+ st.session_state.logs.append("Arrêt manuel de la collecte.")
315
+ st.experimental_rerun()
316
+
317
+ if st.session_state.logs:
318
+ with st.expander("Voir les logs", expanded=True):
319
+ for log in reversed(st.session_state.logs):
320
+ st.text(log)
321
+
322
+ with tabs[1]:
323
+ st.header("Statistiques")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
+ if st.session_state.qa_data:
326
+ df = pd.DataFrame(st.session_state.qa_data)
327
+ st.write(f"Nombre total de paires Q/R générées : **{st.session_state.total_qa_pairs}**")
328
+
329
+ # Visualisation par source
330
+ st.subheader("Paires Q/R par source")
331
+ source_counts = df['source'].value_counts().reset_index()
332
+ source_counts.columns = ['Source', 'Nombre de paires Q/R']
333
+ fig_source = px.bar(source_counts, x='Source', y='Nombre de paires Q/R', title='Distribution par Source', color='Source')
334
+ st.plotly_chart(fig_source, use_container_width=True)
335
+
336
+ # Visualisation par pertinence (si la colonne existe)
337
+ if 'relevance' in df.columns:
338
+ st.subheader("Distribution du score de pertinence")
339
+ fig_relevance = px.histogram(df, x='relevance', nbins=20, title='Distribution des scores de pertinence')
340
+ st.plotly_chart(fig_relevance, use_container_width=True)
341
+
342
+ # Affichage des paires Q/R
343
+ st.subheader("Données collectées")
344
+ st.dataframe(df)
345
+
346
+ csv = df.to_csv(index=False).encode('utf-8')
347
+ st.download_button(
348
+ label="Exporter les données en CSV",
349
+ data=csv,
350
+ file_name=f'donnees_qa_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv',
351
+ mime='text/csv',
352
+ )
353
+
354
+ with tabs[2]:
355
+ st.header("Configuration Avancée")
356
+ st.subheader("Paramètres du serveur LLM")
357
+
358
+ llm_col1, llm_col2 = st.columns(2)
359
+ with llm_col1:
360
+ if st.button("Démarrer le serveur LLM", type="primary", use_container_width=True):
361
+ start_llm_server()
362
+ if st.button("Vérifier le statut du serveur", use_container_width=True):
363
+ check_server_status()
364
+ st.rerun()
365
+ with llm_col2:
366
+ if st.button("Arrêter le serveur LLM", type="secondary", use_container_width=True):
367
+ stop_llm_server()
368
+
369
+ st.markdown("---")
370
+
371
+ st.subheader("Paramètres d'enrichissement IA")
372
+ st.session_state.enable_enrichment = st.checkbox("Activer l'enrichissement IA", value=st.session_state.enable_enrichment, help="Utilise le LLM pour améliorer les paires Q/R.")
373
+ st.session_state.min_relevance = st.slider("Score de pertinence minimum", 0, 100, st.session_state.min_relevance, help="Les contenus en dessous de ce score ne seront pas traités.")
374
+ st.session_state.temperature = st.slider("Température de l'IA", 0.0, 1.0, st.session_state.temperature, help="Contrôle la créativité de l'IA. 0.0 = plus déterministe.")
375
 
376
  if __name__ == "__main__":
377
+ # Cette partie est vide car le code Streamlit est exécuté directement.
378
+ pass