Ferdlance commited on
Commit
093e312
·
verified ·
1 Parent(s): 8c6496b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +682 -278
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
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()
36
  h.ignore_links = True
37
 
38
  # Configuration du logging
@@ -52,327 +52,731 @@ def setup_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
 
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
  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
 
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()