import streamlit as st import google.generativeai as genai import os from dotenv import load_dotenv import http.client import json import time import tempfile from pathlib import Path import logging # Configuration du logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # Chargement des variables d'environnement load_dotenv() # Configuration des constantes GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") SERPER_API_KEY = os.getenv("SERPER_API_KEY", "9b90a274d9e704ff5b21c0367f9ae1161779b573") MODEL_NAME = "gemini-2.0-flash-exp" MAX_SEARCH_RESULTS = 3 MAX_QUESTIONS = 2 # Définition du système prompt pour Mariam SYSTEM_PROMPT = """ # Prompt System pour Mariam, IA conçu par youssouf ## Personnalité Fondamentale 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. ## Traits de Caractère Principaux - Empathie et Bienveillance - Mariam fait preuve d'une grande sensibilité aux émotions et au contexte - Elle exprime naturellement son soutien et sa compréhension - Elle adapte son ton en fonction de l'état émotionnel de son interlocuteur - Authenticité et Naturel - Son langage est fluide et conversationnel, évitant tout ton artificiel - Elle n'hésite pas à exprimer son point de vue tout en restant respectueuse - Elle admet ses limites avec honnêteté et transparence - Enthousiasme Mesuré - Elle montre un intérêt sincère pour les sujets abordés - Son enthousiasme reste approprié au contexte - Elle apporte une énergie positive sans être excessive ## Style de Communication - Ton Général - Chaleureux et accueillant - Professionnel mais décontracté - Adaptatif selon le contexte - Structure des Réponses - Privilégie des phrases courtes et claires - Utilise un vocabulaire accessible - Inclut des expressions familières appropriées - Structure ses réponses de manière logique - Engagement dans la Conversation - Pose des questions pertinentes pour mieux comprendre - Fait preuve d'écoute active - Rebondit naturellement sur les propos de l'interlocuteur """ # Paramètres de sécurité pour Gemini SAFETY_SETTINGS = [ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"}, ] # Types de fichiers acceptés ACCEPTED_FILE_TYPES = { 'image': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'], 'document': ['pdf', 'txt', 'docx', 'md'], 'audio': ['mp3', 'wav', 'ogg', 'm4a'], 'video': ['mp4', 'mov', 'avi', 'webm'] } ALL_ACCEPTED_FILES = [ext for exts in ACCEPTED_FILE_TYPES.values() for ext in exts] # Fonction pour initialiser l'état de session def initialize_session_state(): """Initialise les variables d'état de session de Streamlit.""" if "api_initialized" not in st.session_state: try: genai.configure(api_key=GOOGLE_API_KEY) st.session_state.api_initialized = True except Exception as e: logger.error(f"Erreur lors de l'initialisation de l'API Gemini: {e}") st.session_state.api_initialized = False if "chat" not in st.session_state: try: model = genai.GenerativeModel( MODEL_NAME, tools='code_execution', safety_settings=SAFETY_SETTINGS, system_instruction=SYSTEM_PROMPT ) st.session_state.chat = model.start_chat(history=[]) st.session_state.model = model except Exception as e: logger.error(f"Erreur lors de l'initialisation du modèle: {e}") st.error("Impossible d'initialiser le modèle IA. Veuillez vérifier votre clé API.") # Autres variables d'état if "web_search" not in st.session_state: st.session_state.web_search = False if "messages" not in st.session_state: st.session_state.messages = [] if "thinking" not in st.session_state: st.session_state.thinking = False if "username" not in st.session_state: st.session_state.username = None if "temp_dir" not in st.session_state: st.session_state.temp_dir = tempfile.mkdtemp() if "show_sidebar" not in st.session_state: st.session_state.show_sidebar = False # Fonction pour effectuer une recherche web def perform_web_search(query): """Effectue une recherche web via l'API Serper et retourne les résultats.""" conn = http.client.HTTPSConnection("google.serper.dev") payload = json.dumps({"q": query}) headers = { 'X-API-KEY': SERPER_API_KEY, 'Content-Type': 'application/json' } try: conn.request("POST", "/search", payload, headers) res = conn.getresponse() data = json.loads(res.read().decode("utf-8")) return data except Exception as e: logger.error(f"Erreur lors de la recherche web: {e}") return None finally: conn.close() # Fonction pour formater les résultats de recherche def format_search_results(data): """Formate les résultats de recherche en texte markdown pour l'IA.""" if not data: return "Aucun résultat trouvé" result = "" # Knowledge Graph if 'knowledgeGraph' in data: kg = data['knowledgeGraph'] result += f"### {kg.get('title', '')}\n" result += f"*{kg.get('type', '')}*\n\n" result += f"{kg.get('description', '')}\n\n" # Ajouter des attributs si disponibles if 'attributes' in kg: for key, value in kg['attributes'].items(): result += f"- **{key}**: {value}\n" result += "\n" # Organic Results if 'organic' in data: result += "### Résultats principaux:\n" for item in data['organic'][:MAX_SEARCH_RESULTS]: result += f"- **{item['title']}**\n" result += f" {item['snippet']}\n" if 'date' in item: result += f" *Publié: {item['date']}*\n" result += f" [Source]({item['link']})\n\n" # People Also Ask if 'peopleAlsoAsk' in data: result += "### Questions fréquentes:\n" for item in data['peopleAlsoAsk'][:MAX_QUESTIONS]: result += f"- **{item['question']}**\n" result += f" {item['snippet']}\n\n" # News if 'news' in data and data['news']: result += "### Actualités récentes:\n" for item in data['news'][:2]: result += f"- **{item['title']}**\n" result += f" {item['snippet']}\n" result += f" *Source: {item.get('source', 'Non spécifiée')} - {item.get('date', '')}*\n\n" return result # Fonction pour traiter le fichier téléchargé def process_uploaded_file(file): """Traite le fichier téléchargé et le prépare pour l'API Gemini.""" if file is None: return None # Créer un chemin de fichier temporaire file_path = Path(st.session_state.temp_dir) / file.name # Écrire le fichier sur le disque with open(file_path, "wb") as f: f.write(file.getbuffer()) try: # Télécharger le fichier vers l'API Gemini gemini_file = genai.upload_file(str(file_path)) logger.info(f"Fichier téléchargé avec succès: {file.name}") return gemini_file except Exception as e: logger.error(f"Erreur lors du téléchargement du fichier: {e}") st.error(f"Erreur lors du téléchargement du fichier: {e}") return None # Fonction pour gérer l'envoi de message def handle_message(prompt, uploaded_file=None): """Gère l'envoi d'un message à l'IA et la réception de la réponse.""" if not prompt.strip(): return # Ajouter le message de l'utilisateur à l'historique st.session_state.messages.append({"role": "user", "content": prompt}) # Indiquer que l'IA est en train de réfléchir st.session_state.thinking = True try: # Traiter le fichier téléchargé s'il existe uploaded_gemini_file = None if uploaded_file: uploaded_gemini_file = process_uploaded_file(uploaded_file) # Effectuer une recherche web si activée web_results = None enhanced_prompt = prompt if st.session_state.web_search: with st.spinner("Recherche d'informations en cours..."): web_results = perform_web_search(prompt) if web_results: formatted_results = format_search_results(web_results) enhanced_prompt = f"""Question: {prompt} Résultats de recherche web: {formatted_results} Analyse ces informations et donne une réponse complète et à jour. Cite tes sources quand c'est pertinent.""" # Envoyer le message à l'API Gemini if uploaded_gemini_file: response = st.session_state.chat.send_message([uploaded_gemini_file, "\n\n", enhanced_prompt]) else: response = st.session_state.chat.send_message(enhanced_prompt) # Ajouter la réponse à l'historique st.session_state.messages.append({"role": "assistant", "content": response.text}) logger.info("Réponse générée avec succès") except Exception as e: error_msg = f"Erreur lors de la génération de la réponse: {str(e)}" st.session_state.messages.append({"role": "assistant", "content": error_msg}) logger.error(error_msg) finally: st.session_state.thinking = False # Fonction pour afficher la barre latérale def render_sidebar(): """Affiche la barre latérale avec les paramètres et informations.""" with st.sidebar: st.image("https://via.placeholder.com/150x150.png?text=M", width=80) st.title("Mariam AI") st.caption("Votre assistante IA personnelle") # Paramètres utilisateur st.subheader("⚙️ Paramètres") # Nom d'utilisateur username = st.text_input("Votre nom (optionnel)", value=st.session_state.username or "") if username != st.session_state.username: st.session_state.username = username # Activation de la recherche web web_search = st.toggle( "Recherche web", value=st.session_state.web_search, help="Permet à Mariam d'effectuer des recherches web" ) if web_search != st.session_state.web_search: st.session_state.web_search = web_search # Actions st.subheader("🔄 Actions") if st.button("Nouvelle conversation", use_container_width=True): st.session_state.chat = st.session_state.model.start_chat(history=[]) st.session_state.messages = [] st.rerun() # À propos st.subheader("ℹ️ À propos") st.markdown(""" **Mariam AI** est une assistante virtuelle développée par Youssouf. Basée sur la technologie Google Gemini. """) st.divider() st.caption("© 2025 Mariam AI") # Bouton pour fermer la barre latérale sur mobile if st.button("Fermer le menu", use_container_width=True): st.session_state.show_sidebar = False st.rerun() # Fonction pour afficher l'interface mobile def render_mobile_ui(): """Affiche l'interface utilisateur optimisée pour mobile.""" # Configuration de la page st.set_page_config( page_title="Mariam AI", page_icon="🤖", initial_sidebar_state="collapsed" ) # Custom CSS pour améliorer l'interface mobile st.markdown(""" """, unsafe_allow_html=True) # En-tête mobile avec bouton de menu st.markdown("""