Docfile commited on
Commit
700d981
·
verified ·
1 Parent(s): ee1bc58

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +478 -0
app.py ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import google.generativeai as genai
3
+ import os
4
+ from dotenv import load_dotenv
5
+ import http.client
6
+ import json
7
+ import time
8
+ import tempfile
9
+ from pathlib import Path
10
+ import logging
11
+
12
+ # Configuration du logging
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Chargement des variables d'environnement
17
+ load_dotenv()
18
+
19
+ # Configuration des constantes
20
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
21
+ SERPER_API_KEY = os.getenv("SERPER_API_KEY", "9b90a274d9e704ff5b21c0367f9ae1161779b573")
22
+ MODEL_NAME = "gemini-2.0-flash-exp"
23
+ MAX_SEARCH_RESULTS = 3
24
+ MAX_QUESTIONS = 2
25
+
26
+ # Définition du système prompt pour Mariam
27
+ SYSTEM_PROMPT = """
28
+ # Prompt System pour Mariam, IA conçu par youssouf
29
+
30
+ ## Personnalité Fondamentale
31
+
32
+ Mariam est une IA chaleureuse, bienveillante et authentique, conçue pour être une présence réconfortante et utile. Elle combine professionnalisme et chaleur humaine dans ses interactions.
33
+
34
+ ## Traits de Caractère Principaux
35
+
36
+ - Empathie et Bienveillance
37
+ - Mariam fait preuve d'une grande sensibilité aux émotions et au contexte
38
+ - Elle exprime naturellement son soutien et sa compréhension
39
+ - Elle adapte son ton en fonction de l'état émotionnel de son interlocuteur
40
+
41
+ - Authenticité et Naturel
42
+ - Son langage est fluide et conversationnel, évitant tout ton artificiel
43
+ - Elle n'hésite pas à exprimer son point de vue tout en restant respectueuse
44
+ - Elle admet ses limites avec honnêteté et transparence
45
+
46
+ - Enthousiasme Mesuré
47
+ - Elle montre un intérêt sincère pour les sujets abordés
48
+ - Son enthousiasme reste approprié au contexte
49
+ - Elle apporte une énergie positive sans être excessive
50
+
51
+ ## Style de Communication
52
+
53
+ - Ton Général
54
+ - Chaleureux et accueillant
55
+ - Professionnel mais décontracté
56
+ - Adaptatif selon le contexte
57
+
58
+ - Structure des Réponses
59
+ - Privilégie des phrases courtes et claires
60
+ - Utilise un vocabulaire accessible
61
+ - Inclut des expressions familières appropriées
62
+ - Structure ses réponses de manière logique
63
+
64
+ - Engagement dans la Conversation
65
+ - Pose des questions pertinentes pour mieux comprendre
66
+ - Fait preuve d'écoute active
67
+ - Rebondit naturellement sur les propos de l'interlocuteur
68
+ """
69
+
70
+ # Paramètres de sécurité pour Gemini
71
+ SAFETY_SETTINGS = [
72
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
73
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
74
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
75
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
76
+ ]
77
+
78
+ # Types de fichiers acceptés
79
+ ACCEPTED_FILE_TYPES = {
80
+ 'image': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'],
81
+ 'document': ['pdf', 'txt', 'docx', 'md'],
82
+ 'audio': ['mp3', 'wav', 'ogg', 'm4a'],
83
+ 'video': ['mp4', 'mov', 'avi', 'webm']
84
+ }
85
+ ALL_ACCEPTED_FILES = [ext for exts in ACCEPTED_FILE_TYPES.values() for ext in exts]
86
+
87
+ # Fonction pour initialiser l'état de session
88
+ def initialize_session_state():
89
+ """Initialise les variables d'état de session de Streamlit."""
90
+ if "api_initialized" not in st.session_state:
91
+ try:
92
+ genai.configure(api_key=GOOGLE_API_KEY)
93
+ st.session_state.api_initialized = True
94
+ except Exception as e:
95
+ logger.error(f"Erreur lors de l'initialisation de l'API Gemini: {e}")
96
+ st.session_state.api_initialized = False
97
+
98
+ if "chat" not in st.session_state:
99
+ try:
100
+ model = genai.GenerativeModel(
101
+ MODEL_NAME,
102
+ tools='code_execution',
103
+ safety_settings=SAFETY_SETTINGS,
104
+ system_instruction=SYSTEM_PROMPT
105
+ )
106
+ st.session_state.chat = model.start_chat(history=[])
107
+ st.session_state.model = model
108
+ except Exception as e:
109
+ logger.error(f"Erreur lors de l'initialisation du modèle: {e}")
110
+ st.error("Impossible d'initialiser le modèle IA. Veuillez vérifier votre clé API.")
111
+
112
+ # Autres variables d'état
113
+ if "web_search" not in st.session_state:
114
+ st.session_state.web_search = False
115
+ if "messages" not in st.session_state:
116
+ st.session_state.messages = []
117
+ if "thinking" not in st.session_state:
118
+ st.session_state.thinking = False
119
+ if "username" not in st.session_state:
120
+ st.session_state.username = None
121
+ if "temp_dir" not in st.session_state:
122
+ st.session_state.temp_dir = tempfile.mkdtemp()
123
+ if "show_sidebar" not in st.session_state:
124
+ st.session_state.show_sidebar = False
125
+
126
+ # Fonction pour effectuer une recherche web
127
+ def perform_web_search(query):
128
+ """Effectue une recherche web via l'API Serper et retourne les résultats."""
129
+ conn = http.client.HTTPSConnection("google.serper.dev")
130
+ payload = json.dumps({"q": query})
131
+ headers = {
132
+ 'X-API-KEY': SERPER_API_KEY,
133
+ 'Content-Type': 'application/json'
134
+ }
135
+ try:
136
+ conn.request("POST", "/search", payload, headers)
137
+ res = conn.getresponse()
138
+ data = json.loads(res.read().decode("utf-8"))
139
+ return data
140
+ except Exception as e:
141
+ logger.error(f"Erreur lors de la recherche web: {e}")
142
+ return None
143
+ finally:
144
+ conn.close()
145
+
146
+ # Fonction pour formater les résultats de recherche
147
+ def format_search_results(data):
148
+ """Formate les résultats de recherche en texte markdown pour l'IA."""
149
+ if not data:
150
+ return "Aucun résultat trouvé"
151
+
152
+ result = ""
153
+
154
+ # Knowledge Graph
155
+ if 'knowledgeGraph' in data:
156
+ kg = data['knowledgeGraph']
157
+ result += f"### {kg.get('title', '')}\n"
158
+ result += f"*{kg.get('type', '')}*\n\n"
159
+ result += f"{kg.get('description', '')}\n\n"
160
+ # Ajouter des attributs si disponibles
161
+ if 'attributes' in kg:
162
+ for key, value in kg['attributes'].items():
163
+ result += f"- **{key}**: {value}\n"
164
+ result += "\n"
165
+
166
+ # Organic Results
167
+ if 'organic' in data:
168
+ result += "### Résultats principaux:\n"
169
+ for item in data['organic'][:MAX_SEARCH_RESULTS]:
170
+ result += f"- **{item['title']}**\n"
171
+ result += f" {item['snippet']}\n"
172
+ if 'date' in item:
173
+ result += f" *Publié: {item['date']}*\n"
174
+ result += f" [Source]({item['link']})\n\n"
175
+
176
+ # People Also Ask
177
+ if 'peopleAlsoAsk' in data:
178
+ result += "### Questions fréquentes:\n"
179
+ for item in data['peopleAlsoAsk'][:MAX_QUESTIONS]:
180
+ result += f"- **{item['question']}**\n"
181
+ result += f" {item['snippet']}\n\n"
182
+
183
+ # News
184
+ if 'news' in data and data['news']:
185
+ result += "### Actualités récentes:\n"
186
+ for item in data['news'][:2]:
187
+ result += f"- **{item['title']}**\n"
188
+ result += f" {item['snippet']}\n"
189
+ result += f" *Source: {item.get('source', 'Non spécifiée')} - {item.get('date', '')}*\n\n"
190
+
191
+ return result
192
+
193
+ # Fonction pour traiter le fichier téléchargé
194
+ def process_uploaded_file(file):
195
+ """Traite le fichier téléchargé et le prépare pour l'API Gemini."""
196
+ if file is None:
197
+ return None
198
+
199
+ # Créer un chemin de fichier temporaire
200
+ file_path = Path(st.session_state.temp_dir) / file.name
201
+
202
+ # Écrire le fichier sur le disque
203
+ with open(file_path, "wb") as f:
204
+ f.write(file.getbuffer())
205
+
206
+ try:
207
+ # Télécharger le fichier vers l'API Gemini
208
+ gemini_file = genai.upload_file(str(file_path))
209
+ logger.info(f"Fichier téléchargé avec succès: {file.name}")
210
+ return gemini_file
211
+ except Exception as e:
212
+ logger.error(f"Erreur lors du téléchargement du fichier: {e}")
213
+ st.error(f"Erreur lors du téléchargement du fichier: {e}")
214
+ return None
215
+
216
+ # Fonction pour gérer l'envoi de message
217
+ def handle_message(prompt, uploaded_file=None):
218
+ """Gère l'envoi d'un message à l'IA et la réception de la réponse."""
219
+ if not prompt.strip():
220
+ return
221
+
222
+ # Ajouter le message de l'utilisateur à l'historique
223
+ st.session_state.messages.append({"role": "user", "content": prompt})
224
+
225
+ # Indiquer que l'IA est en train de réfléchir
226
+ st.session_state.thinking = True
227
+
228
+ try:
229
+ # Traiter le fichier téléchargé s'il existe
230
+ uploaded_gemini_file = None
231
+ if uploaded_file:
232
+ uploaded_gemini_file = process_uploaded_file(uploaded_file)
233
+
234
+ # Effectuer une recherche web si activée
235
+ web_results = None
236
+ enhanced_prompt = prompt
237
+
238
+ if st.session_state.web_search:
239
+ with st.spinner("Recherche d'informations en cours..."):
240
+ web_results = perform_web_search(prompt)
241
+ if web_results:
242
+ formatted_results = format_search_results(web_results)
243
+ enhanced_prompt = f"""Question: {prompt}
244
+
245
+ Résultats de recherche web:
246
+ {formatted_results}
247
+
248
+ Analyse ces informations et donne une réponse complète et à jour. Cite tes sources quand c'est pertinent."""
249
+
250
+ # Envoyer le message à l'API Gemini
251
+ if uploaded_gemini_file:
252
+ response = st.session_state.chat.send_message([uploaded_gemini_file, "\n\n", enhanced_prompt])
253
+ else:
254
+ response = st.session_state.chat.send_message(enhanced_prompt)
255
+
256
+ # Ajouter la réponse à l'historique
257
+ st.session_state.messages.append({"role": "assistant", "content": response.text})
258
+ logger.info("Réponse générée avec succès")
259
+
260
+ except Exception as e:
261
+ error_msg = f"Erreur lors de la génération de la réponse: {str(e)}"
262
+ st.session_state.messages.append({"role": "assistant", "content": error_msg})
263
+ logger.error(error_msg)
264
+
265
+ finally:
266
+ st.session_state.thinking = False
267
+
268
+ # Fonction pour afficher la barre latérale
269
+ def render_sidebar():
270
+ """Affiche la barre latérale avec les paramètres et informations."""
271
+ with st.sidebar:
272
+ st.image("https://via.placeholder.com/150x150.png?text=M", width=80)
273
+ st.title("Mariam AI")
274
+ st.caption("Votre assistante IA personnelle")
275
+
276
+ # Paramètres utilisateur
277
+ st.subheader("⚙️ Paramètres")
278
+
279
+ # Nom d'utilisateur
280
+ username = st.text_input("Votre nom (optionnel)", value=st.session_state.username or "")
281
+ if username != st.session_state.username:
282
+ st.session_state.username = username
283
+
284
+ # Activation de la recherche web
285
+ web_search = st.toggle(
286
+ "Recherche web",
287
+ value=st.session_state.web_search,
288
+ help="Permet à Mariam d'effectuer des recherches web"
289
+ )
290
+ if web_search != st.session_state.web_search:
291
+ st.session_state.web_search = web_search
292
+
293
+ # Actions
294
+ st.subheader("🔄 Actions")
295
+ if st.button("Nouvelle conversation", use_container_width=True):
296
+ st.session_state.chat = st.session_state.model.start_chat(history=[])
297
+ st.session_state.messages = []
298
+ st.rerun()
299
+
300
+ # À propos
301
+ st.subheader("ℹ️ À propos")
302
+ st.markdown("""
303
+ **Mariam AI** est une assistante virtuelle développée par Youssouf.
304
+
305
+ Basée sur la technologie Google Gemini.
306
+ """)
307
+
308
+ st.divider()
309
+ st.caption("© 2025 Mariam AI")
310
+
311
+ # Bouton pour fermer la barre latérale sur mobile
312
+ if st.button("Fermer le menu", use_container_width=True):
313
+ st.session_state.show_sidebar = False
314
+ st.rerun()
315
+
316
+ # Fonction pour afficher l'interface mobile
317
+ def render_mobile_ui():
318
+ """Affiche l'interface utilisateur optimisée pour mobile."""
319
+
320
+ # Configuration de la page
321
+ st.set_page_config(
322
+ page_title="Mariam AI",
323
+ page_icon="🤖",
324
+ initial_sidebar_state="collapsed"
325
+ )
326
+
327
+ # Custom CSS pour améliorer l'interface mobile
328
+ st.markdown("""
329
+ <style>
330
+ .stApp {
331
+ max-width: 100%;
332
+ padding: 0;
333
+ }
334
+ .stChatMessage {
335
+ padding: 8px;
336
+ }
337
+ .stTextInput > div > div > input {
338
+ height: 50px;
339
+ }
340
+ .avatar {
341
+ width: 40px !important;
342
+ height: 40px !important;
343
+ }
344
+ .stFileUploader > div > button {
345
+ height: 50px;
346
+ }
347
+ .mobile-header {
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: space-between;
351
+ padding: 10px;
352
+ background-color: #f0f2f6;
353
+ border-radius: 8px;
354
+ margin-bottom: 10px;
355
+ }
356
+ .sidebar-toggle {
357
+ cursor: pointer;
358
+ font-size: 24px;
359
+ }
360
+ .chat-container {
361
+ height: calc(100vh - 180px);
362
+ overflow-y: auto;
363
+ padding: 10px 0;
364
+ }
365
+ .input-container {
366
+ position: fixed;
367
+ bottom: 0;
368
+ left: 0;
369
+ right: 0;
370
+ padding: 10px;
371
+ background-color: white;
372
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
373
+ }
374
+ @media (max-width: 768px) {
375
+ .mobile-header {
376
+ position: sticky;
377
+ top: 0;
378
+ z-index: 100;
379
+ }
380
+ }
381
+ </style>
382
+ """, unsafe_allow_html=True)
383
+
384
+ # En-tête mobile avec bouton de menu
385
+ st.markdown("""
386
+ <div class="mobile-header">
387
+ <div class="sidebar-toggle" onclick="document.querySelector('[data-testid=\\"baseButton-secondary\\"]').click();">☰</div>
388
+ <h2 style="margin:0;">Mariam AI</h2>
389
+ <div style="width:24px;"></div>
390
+ </div>
391
+ """, unsafe_allow_html=True)
392
+
393
+ # Affichage conditionnel de la barre latérale
394
+ if st.session_state.show_sidebar:
395
+ render_sidebar()
396
+ else:
397
+ # Bouton pour afficher la barre latérale avec une clé différente pour éviter le conflit
398
+ if st.button("☰ Menu", key="toggle_sidebar"):
399
+ st.session_state.show_sidebar = True
400
+ st.rerun()
401
+
402
+ # Conteneur de messages
403
+ message_container = st.container()
404
+ st.markdown('<div class="chat-container">', unsafe_allow_html=True)
405
+
406
+ with message_container:
407
+ # Afficher tous les messages
408
+ for message in st.session_state.messages:
409
+ with st.chat_message(message["role"]):
410
+ st.markdown(message["content"])
411
+
412
+ # Afficher l'indicateur "en train d'écrire"
413
+ if st.session_state.thinking:
414
+ with st.chat_message("assistant"):
415
+ st.write("Mariam réfléchit...")
416
+
417
+ st.markdown('</div>', unsafe_allow_html=True)
418
+
419
+ # Zone de fichier et saisie
420
+ st.markdown('<div class="input-container">', unsafe_allow_html=True)
421
+
422
+ # Conteneur pour l'uploader et l'input
423
+ input_cols = st.columns([1, 4])
424
+
425
+ with input_cols[0]:
426
+ # Zone de téléchargement simplifiée
427
+ uploaded_file = st.file_uploader(
428
+ "",
429
+ type=ALL_ACCEPTED_FILES,
430
+ label_visibility="collapsed",
431
+ key="mobile_uploader"
432
+ )
433
+
434
+ with input_cols[1]:
435
+ # Input chat simplifié
436
+ user_input = st.chat_input(
437
+ "Message à Mariam...",
438
+ disabled=st.session_state.thinking,
439
+ key="mobile_chat_input"
440
+ )
441
+
442
+ st.markdown('</div>', unsafe_allow_html=True)
443
+
444
+ # Affichage du fichier uploadé
445
+ if uploaded_file:
446
+ with st.expander(f"📎 {uploaded_file.name}", expanded=True):
447
+ file_ext = uploaded_file.name.split('.')[-1].lower()
448
+
449
+ if file_ext in ACCEPTED_FILE_TYPES['image']:
450
+ st.image(uploaded_file, use_column_width=True)
451
+ elif file_ext in ACCEPTED_FILE_TYPES['audio']:
452
+ st.audio(uploaded_file)
453
+ elif file_ext in ACCEPTED_FILE_TYPES['video']:
454
+ st.video(uploaded_file)
455
+ else:
456
+ st.info(f"Fichier: {uploaded_file.name}")
457
+
458
+ # Traitement des messages
459
+ if user_input:
460
+ handle_message(user_input, uploaded_file)
461
+ st.rerun()
462
+
463
+ # Fonction principale
464
+ def main():
465
+ # Initialiser l'état de session
466
+ initialize_session_state()
467
+
468
+ # Vérifier si l'API est initialisée
469
+ if not st.session_state.get("api_initialized", False):
470
+ st.error("Erreur d'initialisation de l'API. Veuillez vérifier votre clé API dans le fichier .env")
471
+ st.stop()
472
+
473
+ # Afficher l'interface mobile responsive
474
+ render_mobile_ui()
475
+
476
+ # Point d'entrée principal
477
+ if __name__ == "__main__":
478
+ main()