Browse Source

Version 2.7.0

Johnounet 1 năm trước cách đây
mục cha
commit
855bd36247
3 tập tin đã thay đổi với 318 bổ sung265 xóa
  1. 303 260
      chatbot.py
  2. 11 0
      image_analysis_prompt.txt
  3. 4 5
      requirements.txt

+ 303 - 260
chatbot.py

@@ -1,22 +1,22 @@
 import os
-import mysql.connector
-from mysql.connector import Error
-import base64
 import json
 import logging
-import re
+import base64
 from io import BytesIO
+import asyncio
 
+import mysql.connector
+from mysql.connector import Error
+from PIL import Image
+import tiktoken
 import discord
 from discord.ext import commands
 from dotenv import load_dotenv
-from PIL import Image
-import tiktoken
 from openai import AsyncOpenAI, OpenAIError
 
-# ================================
+# =================================
 # Configuration et Initialisation
-# ================================
+# =================================
 
 # Charger les variables d'environnement depuis le fichier .env
 load_dotenv()
@@ -24,31 +24,34 @@ DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
 OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
 DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
 PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
-CONVERSATION_HISTORY_FILE = os.getenv('CONVERSATION_HISTORY_FILE', 'conversation_history.json')
+IMAGE_ANALYSIS_PROMPT_FILE = os.getenv('IMAGE_ANALYSIS_PROMPT_FILE', 'image_analysis_prompt.txt')
 BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
-BOT_VERSION = "2.6.0"
+BOT_VERSION = "2.7.0"
 
 # Validation des variables d'environnement
 required_env_vars = {
     'DISCORD_TOKEN': DISCORD_TOKEN,
     'OPENAI_API_KEY': OPENAI_API_KEY,
-    'DISCORD_CHANNEL_ID': DISCORD_CHANNEL_ID
+    'DISCORD_CHANNEL_ID': DISCORD_CHANNEL_ID,
+    'IMAGE_ANALYSIS_PROMPT_FILE': IMAGE_ANALYSIS_PROMPT_FILE
 }
 
 missing_vars = [var for var, val in required_env_vars.items() if val is None]
 if missing_vars:
     raise ValueError(f"Les variables d'environnement suivantes ne sont pas définies: {', '.join(missing_vars)}")
 
-# Vérification de l'existence du fichier de prompt de personnalité
-if not os.path.isfile(PERSONALITY_PROMPT_FILE):
-    raise FileNotFoundError(f"Le fichier de prompt de personnalité '{PERSONALITY_PROMPT_FILE}' est introuvable.")
+# Vérification de l'existence des fichiers de prompt
+for file_var, file_path in [('PERSONALITY_PROMPT_FILE', PERSONALITY_PROMPT_FILE),
+                            ('IMAGE_ANALYSIS_PROMPT_FILE', IMAGE_ANALYSIS_PROMPT_FILE)]:
+    if not os.path.isfile(file_path):
+        raise FileNotFoundError(f"Le fichier de prompt '{file_var}' '{file_path}' est introuvable.")
 
-# Lire le prompt de personnalité depuis le fichier
+# Lire les prompts depuis les fichiers
 with open(PERSONALITY_PROMPT_FILE, 'r', encoding='utf-8') as f:
     PERSONALITY_PROMPT = f.read().strip()
 
-# Initialiser le client OpenAI asynchrone
-openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
+with open(IMAGE_ANALYSIS_PROMPT_FILE, 'r', encoding='utf-8') as f:
+    IMAGE_ANALYSIS_PROMPT = f.read().strip()
 
 # Configurer les logs
 LOG_FORMAT = '%(asctime)s : %(name)s : %(levelname)s : %(message)s'
@@ -62,25 +65,14 @@ logging.basicConfig(
 )
 logger = logging.getLogger(BOT_NAME)
 logger.setLevel(logging.INFO)
-
-# Réduire le niveau de log pour certaines librairies
-logging.getLogger('httpx').setLevel(logging.WARNING)
+logging.getLogger('httpx').setLevel(logging.WARNING)  # Réduire le niveau de log pour 'httpx'
 
 # Initialiser les intents Discord
 intents = discord.Intents.default()
 intents.message_content = True
 
-# Initialiser le client Discord
-class MyDiscordClient(discord.Client):
-    def __init__(self, **options):
-        super().__init__(**options)
-
-    async def close(self):
-        if openai_client:
-            await openai_client.close()
-        await super().close()
-
-client_discord = MyDiscordClient(intents=intents)
+# Initialiser le client OpenAI asynchrone
+openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
 
 # Convertir l'ID du canal Discord en entier
 try:
@@ -88,61 +80,96 @@ try:
 except ValueError:
     raise ValueError("L'ID du canal Discord est invalide. Assurez-vous qu'il s'agit d'un entier.")
 
-# ========================
-# Configuration de la base de données
-# ========================
+# =====================================
+# Gestion de la Base de Données MariaDB
+# =====================================
 
-def create_db_connection():
-    try:
-        connection = mysql.connector.connect(
-            host=os.getenv('DB_HOST'),
-            user=os.getenv('DB_USER'),
-            password=os.getenv('DB_PASSWORD'),
-            database=os.getenv('DB_NAME'),
-            charset='utf8mb4',
-            collation='utf8mb4_unicode_ci'
-        )
-        if connection.is_connected():
-            logger.info("Connexion réussie à MariaDB")
-            return connection
-    except Error as e:
-        logger.error(f"Erreur de connexion à MariaDB: {e}")
+class DatabaseManager:
+    def __init__(self):
+        self.connection = self.create_db_connection()
+
+    def create_db_connection(self):
+        try:
+            connection = mysql.connector.connect(
+                host=os.getenv('DB_HOST'),
+                user=os.getenv('DB_USER'),
+                password=os.getenv('DB_PASSWORD'),
+                database=os.getenv('DB_NAME'),
+                charset='utf8mb4',
+                collation='utf8mb4_unicode_ci'
+            )
+            if connection.is_connected():
+                logger.info("Connexion réussie à MariaDB")
+                return connection
+        except Error as e:
+            logger.error(f"Erreur de connexion à MariaDB: {e}")
         return None
 
-# ========================
-# Gestion du chargement et de la sauvegarde de l'Historique
-# ========================
+    def load_conversation_history(self):
+        global conversation_history
+        try:
+            with self.connection.cursor(dictionary=True) as cursor:
+                cursor.execute("SELECT role, content FROM conversation_history ORDER BY id ASC")
+                rows = cursor.fetchall()
+                conversation_history = [
+                    row for row in rows
+                    if not (row['role'] == "system" and row['content'] == PERSONALITY_PROMPT)
+                ]
+            logger.info("Historique chargé depuis la base de données")
+        except Error as e:
+            logger.error(f"Erreur lors du chargement de l'historique depuis la base de données: {e}")
+            conversation_history = []
+
+    def save_message(self, role, content):
+        try:
+            with self.connection.cursor() as cursor:
+                sql = "INSERT INTO conversation_history (role, content) VALUES (%s, %s)"
+                cursor.execute(sql, (role, json.dumps(content, ensure_ascii=False) if isinstance(content, (dict, list)) else content))
+            self.connection.commit()
+            logger.debug(f"Message sauvegardé dans la base de données: {role} - {content[:50]}...")
+        except Error as e:
+            logger.error(f"Erreur lors de la sauvegarde du message dans la base de données: {e}")
+
+    def delete_old_image_analyses(self):
+        try:
+            with self.connection.cursor() as cursor:
+                cursor.execute("DELETE FROM conversation_history WHERE role = 'system' AND content LIKE '__IMAGE_ANALYSIS__:%'")
+            self.connection.commit()
+            logger.info("Toutes les anciennes analyses d'image ont été supprimées de la base de données.")
+        except Error as e:
+            logger.error(f"Erreur lors de la suppression des analyses d'image: {e}")
+
+    def reset_history(self):
+        try:
+            with self.connection.cursor() as cursor:
+                cursor.execute("DELETE FROM conversation_history")
+            self.connection.commit()
+            logger.info("Historique des conversations réinitialisé.")
+        except Error as e:
+            logger.error(f"Erreur lors de la réinitialisation de l'historique: {e}")
+
+    def delete_old_messages(self, limit):
+        try:
+            with self.connection.cursor() as cursor:
+                cursor.execute("DELETE FROM conversation_history ORDER BY id ASC LIMIT %s", (limit,))
+            self.connection.commit()
+            logger.debug(f"{limit} messages les plus anciens ont été supprimés de la base de données pour maintenir l'historique à 150 messages.")
+        except Error as e:
+            logger.error(f"Erreur lors de la suppression des anciens messages: {e}")
+
+    def close_connection(self):
+        if self.connection and self.connection.is_connected():
+            self.connection.close()
+            logger.info("Connexion à la base de données fermée.")
+
+# ===============================
+# Gestion de l'Historique des Messages
+# ===============================
 
 conversation_history = []
 last_analysis_index = None
 messages_since_last_analysis = 0
 
-def load_conversation_history(db_connection):
-    global conversation_history
-    try:
-        cursor = db_connection.cursor(dictionary=True)
-        cursor.execute("SELECT role, content FROM conversation_history ORDER BY id ASC")
-        rows = cursor.fetchall()
-        conversation_history = [row for row in rows if not (row['role'] == "system" and row['content'] == PERSONALITY_PROMPT)]
-        logger.info("Historique chargé depuis la base de données")
-    except Error as e:
-        logger.error(f"Erreur lors du chargement de l'historique depuis la base de données: {e}")
-        conversation_history = []
-    finally:
-        cursor.close()
-
-def save_message_to_db(db_connection, role, content):
-    try:
-        cursor = db_connection.cursor()
-        sql = "INSERT INTO conversation_history (role, content) VALUES (%s, %s)"
-        cursor.execute(sql, (role, json.dumps(content) if isinstance(content, (dict, list)) else content))
-        db_connection.commit()
-        logger.debug(f"Message sauvegardé dans la base de données: {role} - {content[:50]}...")
-    except Error as e:
-        logger.error(f"Erreur lors de la sauvegarde du message dans la base de données: {e}")
-    finally:
-        cursor.close()
-
 # ====================
 # Fonctions Utilitaires
 # ====================
@@ -237,9 +264,9 @@ async def encode_image_from_attachment(attachment, mode='high'):
     resized_image = resize_image(image_data, mode=mode, attachment_filename=attachment.filename)
     return base64.b64encode(resized_image).decode('utf-8')
 
-# ========================
+# =================================
 # Interaction avec OpenAI
-# ========================
+# =================================
 
 # Charger l'encodeur pour le modèle GPT-4o mini
 encoding = tiktoken.get_encoding("o200k_base")
@@ -260,7 +287,7 @@ async def call_openai_model(model, messages, max_tokens, temperature=0.8):
             # Ne pas logger les réponses de 'gpt-4o-mini' et 'gpt-4o'
             if model not in ["gpt-4o-mini", "gpt-4o"]:
                 logger.info(f"Réponse de {model}: {reply[:100]}...")
-            
+
             if hasattr(response, 'usage') and response.usage:
                 usage = {
                     'prompt_tokens': response.usage.prompt_tokens,
@@ -282,16 +309,7 @@ async def call_openai_model(model, messages, max_tokens, temperature=0.8):
 
 async def call_gpt4o_for_image_analysis(image_data, user_text=None, detail='high'):
     """Appelle GPT-4o pour analyser une image."""
-    prompt = (
-        "Tu es un expert en analyse d'images et de textes, spécialisé dans l'étude du corps humain. "
-        "On te présente une image ou un texte qui pourrait contenir des informations importantes. "
-        "Analyse chaque détail de manière méticuleuse. "
-        "Si l'image montre un environnement sans personnage, décris minutieusement les objets, leur disposition, les couleurs, textures, formes, et tout autre élément notable. "
-        "Si du texte est présent, analyse chaque mot attentivement : style, mise en page, ou tout détail subtil qui pourrait en révéler plus sur le contexte ou l'intention. "
-        "Si des personnages sont présents, décris-les avec précision, dans l'ordre suivant : D'abord leur posture et expression faciale. "
-        "Puis leurs vêtements dans leur intégralité. "
-        "Puis leur physique, en commençant par les cheveux, visage, épaules, poitrine, ventre, taille, hanches, fesses, cuisses, jambes et pieds."
-    )
+    prompt = IMAGE_ANALYSIS_PROMPT
     if user_text:
         prompt += f" Voici ce que l'on te décrit : \"{user_text}\"."
     
@@ -333,14 +351,12 @@ async def call_gpt4o_mini_with_analysis(analysis_text, user_name, user_question,
 
     if has_text_flag:
         user_content = (
-            f"Tu es Rika, une femme de 24 ans et membre à part entière du serveur Discord sur lequel tu te trouves. "
             f"{user_name} a posté un message contenant une image et a écrit avec : '{user_question}'. "
             "Réponds à l'utilisateur en te basant sur l'analyse, avec ta personnalité. "
             "Ne mentionne pas explicitement que l'analyse est pré-existante, fais comme si tu l'avais faite toi-même."
         )
     else:
         user_content = (
-            f"Tu es Rika, une femme de 24 ans et membre à part entière du serveur Discord sur lequel tu te trouves. "
             f"{user_name} a partagé une image sans texte additionnel. "
             "Commente l'image en te basant sur l'analyse, avec ta personnalité. "
             "Ne mentionne pas que l'analyse a été fournie à l'avance, réagis comme si tu l'avais toi-même effectuée."
@@ -393,11 +409,11 @@ async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
     
     return reply
 
-# ============================
-# Gestion du contenu de l'Historique
-# ============================
+# =====================================
+# Gestion du Contenu de l'Historique
+# =====================================
 
-async def remove_old_image_analyses(db_connection, new_analysis=False):
+async def remove_old_image_analyses(db_manager, new_analysis=False):
     """Supprime les anciennes analyses d'images de l'historique."""
     global conversation_history, last_analysis_index, messages_since_last_analysis
 
@@ -411,17 +427,12 @@ async def remove_old_image_analyses(db_connection, new_analysis=False):
         messages_since_last_analysis = 0
 
         # Supprimer les analyses d'images de la base de données
-        try:
-            cursor = db_connection.cursor()
-            cursor.execute("DELETE FROM conversation_history WHERE role = 'system' AND content LIKE '__IMAGE_ANALYSIS__:%'")
-            db_connection.commit()
-            logger.info("Toutes les anciennes analyses d'image ont été supprimées de la base de données.")
-        except Error as e:
-            logger.error(f"Erreur lors de la suppression des analyses d'image: {e}")
-        finally:
-            cursor.close()
+        db_manager.delete_old_image_analyses()
+    else:
+        # Exemple de logique additionnelle si nécessaire
+        pass
 
-async def add_to_conversation_history(db_connection, new_message):
+async def add_to_conversation_history(db_manager, new_message):
     global conversation_history, last_analysis_index, messages_since_last_analysis
 
     # Exclure le PERSONALITY_PROMPT de l'historique
@@ -431,12 +442,12 @@ async def add_to_conversation_history(db_connection, new_message):
 
     # Gérer les analyses d'images
     if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
-        await remove_old_image_analyses(db_connection, new_analysis=True)
+        await remove_old_image_analyses(db_manager, new_analysis=True)
 
     # Ajouter le message à l'historique en mémoire
     conversation_history.append(new_message)
     # Sauvegarder dans la base de données
-    save_message_to_db(db_connection, new_message.get("role"), new_message.get("content"))
+    db_manager.save_message(new_message.get("role"), new_message.get("content"))
 
     logger.debug(f"Message ajouté à l'historique. Taille actuelle : {len(conversation_history)}")
 
@@ -445,193 +456,225 @@ async def add_to_conversation_history(db_connection, new_message):
         last_analysis_index = len(conversation_history) - 1
         messages_since_last_analysis = 0
     else:
-        await remove_old_image_analyses(db_connection, new_analysis=False)
+        await remove_old_image_analyses(db_manager, new_analysis=False)
 
     # Limiter l'historique à 150 messages
     if len(conversation_history) > 150:
         excess = len(conversation_history) - 150
         conversation_history = conversation_history[excess:]
         # Supprimer les messages les plus anciens de la base de données
-        try:
-            cursor = db_connection.cursor()
-            cursor.execute("DELETE FROM conversation_history ORDER BY id ASC LIMIT %s", (excess,))
-            db_connection.commit()
-            logger.debug(f"{excess} messages les plus anciens ont été supprimés de la base de données pour maintenir l'historique à 150 messages.")
-        except Error as e:
-            logger.error(f"Erreur lors de la suppression des anciens messages: {e}")
-        finally:
-            cursor.close()
+        db_manager.delete_old_messages(excess)
 
-# =====================
+# =====================================
 # Gestion des Événements Discord
-# =====================
-
-@client_discord.event
-async def on_ready():
-    """Événement déclenché lorsque le bot est prêt."""
-    logger.info(f'{BOT_NAME} connecté en tant que {client_discord.user}')
-
-    if not conversation_history:
-        logger.info("Aucun historique trouvé. L'historique commence vide.")
+# =====================================
 
-    # Envoyer un message de version dans le canal Discord
-    channel = client_discord.get_channel(chatgpt_channel_id)
-    if channel:
-        try:
-            embed = discord.Embed(
-                title="Bot Démarré",
-                description=f"🎉 {BOT_NAME} est en ligne ! Version {BOT_VERSION}",
-                color=0x00ff00  # Vert
-            )
-            await channel.send(embed=embed)
-            logger.info(f"Message de connexion envoyé dans le canal ID {chatgpt_channel_id}")
-        except discord.Forbidden:
-            logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal ID {chatgpt_channel_id}.")
-        except discord.HTTPException as e:
-            logger.error(f"Erreur lors de l'envoi du message de connexion : {e}")
-    else:
-        logger.error(f"Canal avec ID {chatgpt_channel_id} non trouvé.")
+class MyDiscordClient(discord.Client):
+    def __init__(self, db_manager, **options):
+        super().__init__(**options)
+        self.db_manager = db_manager
+        self.message_queue = asyncio.Queue()
 
-@client_discord.event
-async def on_message(message):
-    """Événement déclenché lorsqu'un message est envoyé dans un canal suivi."""
-    global conversation_history, last_analysis_index, messages_since_last_analysis
+    async def setup_hook(self):
+        """Hook d'initialisation asynchrone pour configurer des tâches supplémentaires."""
+        self.processing_task = asyncio.create_task(self.process_messages())
 
-    # Ignorer les messages provenant d'autres canaux ou du bot lui-même
-    if message.channel.id != chatgpt_channel_id or message.author == client_discord.user:
-        return
-
-    user_text = message.content.strip()
+    async def close(self):
+        if openai_client:
+            await openai_client.close()
+        self.db_manager.close_connection()
+        self.processing_task.cancel()
+        await super().close()
 
-    # Commande de réinitialisation de l'historique
-    if user_text.lower() == "!reset_history":
-        if not message.author.guild_permissions.administrator:
-            await message.channel.send("❌ Vous n'avez pas la permission d'utiliser cette commande.")
+    async def on_ready(self):
+        """Événement déclenché lorsque le bot est prêt."""
+        logger.info(f'{BOT_NAME} connecté en tant que {self.user}')
+
+        if not conversation_history:
+            logger.info("Aucun historique trouvé. L'historique commence vide.")
+
+        # Envoyer un message de version dans le canal Discord
+        channel = self.get_channel(chatgpt_channel_id)
+        if channel:
+            try:
+                embed = discord.Embed(
+                    title="Bot Démarré",
+                    description=f"🎉 {BOT_NAME} est en ligne ! Version {BOT_VERSION}",
+                    color=0x00ff00  # Vert
+                )
+                await channel.send(embed=embed)
+                logger.info(f"Message de connexion envoyé dans le canal ID {chatgpt_channel_id}")
+            except discord.Forbidden:
+                logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal ID {chatgpt_channel_id}.")
+            except discord.HTTPException as e:
+                logger.error(f"Erreur lors de l'envoi du message de connexion : {e}")
+        else:
+            logger.error(f"Canal avec ID {chatgpt_channel_id} non trouvé.")
+
+    async def on_message(self, message):
+        """Événement déclenché lorsqu'un message est envoyé dans un canal suivi."""
+
+        # Ignorer les messages provenant d'autres canaux ou du bot lui-même
+        if message.channel.id != chatgpt_channel_id or message.author == self.user:
             return
 
-        conversation_history = []
-        try:
-            cursor = db_connection.cursor()
-            cursor.execute("DELETE FROM conversation_history")
-            db_connection.commit()
-            logger.info(f"Historique des conversations réinitialisé par {message.author}.")
+        await self.message_queue.put(message)
+
+    async def process_messages(self):
+        """Tâche en arrière-plan pour traiter les messages séquentiellement."""
+        while True:
+            message = await self.message_queue.get()
+            try:
+                await self.handle_message(message)
+            except Exception as e:
+                logger.error(f"Erreur lors du traitement du message : {e}")
+                try:
+                    await message.channel.send("Une erreur est survenue lors du traitement de votre message.")
+                except Exception as send_error:
+                    logger.error(f"Erreur lors de l'envoi du message d'erreur : {send_error}")
+            finally:
+                self.message_queue.task_done()
+
+    async def handle_message(self, message):
+        """Fonction pour traiter un seul message."""
+        global conversation_history, last_analysis_index, messages_since_last_analysis
+
+        user_text = message.content.strip()
+
+        # Commande de réinitialisation de l'historique
+        if user_text.lower() == "!reset_history":
+            if not message.author.guild_permissions.administrator:
+                await message.channel.send("❌ Vous n'avez pas la permission d'utiliser cette commande.")
+                return
+
+            conversation_history = []
+            self.db_manager.reset_history()
             await message.channel.send("✅ L'historique des conversations a été réinitialisé.")
-        except Error as e:
-            logger.error(f"Erreur lors de la réinitialisation de l'historique: {e}")
-            await message.channel.send("❌ Une erreur est survenue lors de la réinitialisation de l'historique.")
-        finally:
-            cursor.close()
-        return
+            return
 
-    # Traiter les pièces jointes
-    image_data = None
-    file_content = None
-    attachment_filename = None
-    allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
-
-    if message.attachments:
-        for attachment in message.attachments:
-            if any(attachment.filename.lower().endswith(ext) for ext in allowed_extensions):
-                file_content = await read_text_file(attachment)
-                attachment_filename = attachment.filename
-                break
-            elif attachment.content_type and attachment.content_type.startswith('image/'):
-                image_data = await encode_image_from_attachment(attachment, mode='high')
-                break
-
-    # Traitement des images
-    if image_data:
-        has_user_text = has_text(user_text)
-        user_text_to_use = user_text if has_user_text else None
+        # Traiter les pièces jointes
+        image_data = None
+        file_content = None
+        attachment_filename = None
+        allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
+
+        if message.attachments:
+            for attachment in message.attachments:
+                if any(attachment.filename.lower().endswith(ext) for ext in allowed_extensions):
+                    file_content = await read_text_file(attachment)
+                    attachment_filename = attachment.filename
+                    break
+                elif attachment.content_type and attachment.content_type.startswith('image/'):
+                    image_data = await encode_image_from_attachment(attachment, mode='high')
+                    break
+
+        # Traitement des images
+        if image_data:
+            has_user_text = has_text(user_text)
+            user_text_to_use = user_text if has_user_text else None
+
+            temp_msg = await message.channel.send(f"*{BOT_NAME} observe l'image...*")
+
+            try:
+                # Analyser l'image avec GPT-4o
+                analysis = await call_gpt4o_for_image_analysis(image_data, user_text=user_text_to_use)
+
+                if analysis:
+                    # Ajouter l'analyse à l'historique
+                    analysis_message = {
+                        "role": "system",
+                        "content": f"__IMAGE_ANALYSIS__:{analysis}"
+                    }
+                    await add_to_conversation_history(self.db_manager, analysis_message)
+
+                    # Générer une réponse basée sur l'analyse
+                    reply = await call_gpt4o_mini_with_analysis(analysis, message.author.name, user_text, has_user_text)
+                    if reply:
+                        await temp_msg.delete()
+                        await message.channel.send(reply)
+
+                        # Construire et ajouter les messages à l'historique
+                        user_message_text = f"{user_text} (a posté une image.)" if has_user_text else (
+                            "Une image a été postée, mais elle n'est pas disponible pour analyse directe. Veuillez vous baser uniquement sur l'analyse fournie."
+                        )
+                        user_message = {
+                            "role": "user",
+                            "content": f"{message.author.name} dit : {user_message_text}"
+                        }
+                        assistant_message = {
+                            "role": "assistant",
+                            "content": reply
+                        }
+
+                        await add_to_conversation_history(self.db_manager, user_message)
+                        await add_to_conversation_history(self.db_manager, assistant_message)
+                    else:
+                        await temp_msg.delete()
+                        await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
+                else:
+                    await temp_msg.delete()
+                    await message.channel.send("Désolé, je n'ai pas pu analyser l'image.")
 
-        temp_msg = await message.channel.send(f"*{BOT_NAME} observe l'image...*")
+            except Exception as e:
+                await temp_msg.delete()
+                await message.channel.send("Une erreur est survenue lors du traitement de l'image.")
+                logger.error(f"Erreur lors du traitement de l'image: {e}")
 
-        try:
-            # Analyser l'image avec GPT-4o
-            analysis = await call_gpt4o_for_image_analysis(image_data, user_text=user_text_to_use)
-
-            if analysis:
-                # Ajouter l'analyse à l'historique
-                analysis_message = {
-                    "role": "system",
-                    "content": f"__IMAGE_ANALYSIS__:{analysis}"
-                }
-                await add_to_conversation_history(db_connection, analysis_message)
+            return  # Ne pas continuer le traitement après une image
+
+        # Ajouter le contenu du fichier au texte de l'utilisateur si un fichier est présent
+        if file_content:
+            user_text += f"\nContenu du fichier {attachment_filename}:\n{file_content}"
 
-                # Générer une réponse basée sur l'analyse
-                reply = await call_gpt4o_mini_with_analysis(analysis, message.author.name, user_text, has_user_text)
+        # Vérifier si le texte n'est pas vide
+        if not has_text(user_text):
+            return  # Ne pas appeler l'API si le texte est vide
+
+        async with message.channel.typing():
+            try:
+                # Appeler l'API OpenAI pour le texte
+                reply = await call_openai_api(user_text, message.author.name)
                 if reply:
-                    await temp_msg.delete()
                     await message.channel.send(reply)
 
                     # Construire et ajouter les messages à l'historique
-                    user_message_text = f"{user_text} (a posté une image.)" if has_user_text else (
-                        "Une image a été postée, mais elle n'est pas disponible pour analyse directe. Veuillez vous baser uniquement sur l'analyse fournie."
-                    )
                     user_message = {
                         "role": "user",
-                        "content": [
-                            {"type": "text", "text": f"{message.author.name} dit : {user_message_text}"}
-                        ]
+                        "content": f"{message.author.name} dit : {user_text}"
                     }
+
                     assistant_message = {
                         "role": "assistant",
                         "content": reply
                     }
 
-                    await add_to_conversation_history(db_connection, user_message)
-                    await add_to_conversation_history(db_connection, assistant_message)
+                    await add_to_conversation_history(self.db_manager, user_message)
+                    await add_to_conversation_history(self.db_manager, assistant_message)
                 else:
-                    await temp_msg.delete()
                     await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
-            else:
-                await temp_msg.delete()
-                await message.channel.send("Désolé, je n'ai pas pu analyser l'image.")
-
-        except Exception as e:
-            await temp_msg.delete()
-            await message.channel.send("Une erreur est survenue lors du traitement de l'image.")
-            logger.error(f"Erreur lors du traitement de l'image: {e}")
-
-        return  # Ne pas continuer le traitement après une image
-
-    # Ajouter le contenu du fichier au texte de l'utilisateur si un fichier est présent
-    if file_content:
-        user_text += f"\nContenu du fichier {attachment_filename}:\n{file_content}"
-
-    # Vérifier si le texte n'est pas vide
-    if not has_text(user_text):
-        return  # Ne pas appeler l'API si le texte est vide
-
-    # Appeler l'API OpenAI pour le texte
-    reply = await call_openai_api(user_text, message.author.name)
-    if reply:
-        await message.channel.send(reply)
-
-        # Construire et ajouter les messages à l'historique
-        user_message = {
-            "role": "user",
-            "content": [
-                {"type": "text", "text": f"{message.author.name} dit : {user_text}"}
-            ]
-        }
-        assistant_message = {
-            "role": "assistant",
-            "content": reply
-        }
-
-        await add_to_conversation_history(db_connection, user_message)
-        await add_to_conversation_history(db_connection, assistant_message)
+            except Exception as e:
+                await message.channel.send("Une erreur est survenue lors de la génération de la réponse.")
+                logger.error(f"Erreur lors du traitement du texte: {e}")
 
 # ============================
 # Démarrage du Bot Discord
 # ============================
 
-if __name__ == "__main__":
-    db_connection = create_db_connection()
-    if db_connection:
-        load_conversation_history(db_connection)
-        client_discord.run(DISCORD_TOKEN)
-        db_connection.close()
-    else:
+def main():
+    db_manager = DatabaseManager()
+    if not db_manager.connection:
         logger.error("Le bot ne peut pas démarrer sans connexion à la base de données.")
+        return
+
+    db_manager.load_conversation_history()
+
+    client_discord = MyDiscordClient(db_manager=db_manager, intents=intents)
+    try:
+        client_discord.run(DISCORD_TOKEN)
+    except Exception as e:
+        logger.error(f"Erreur lors du démarrage du bot Discord: {e}")
+    finally:
+        db_manager.close_connection()
+
+if __name__ == "__main__":
+    main()

+ 11 - 0
image_analysis_prompt.txt

@@ -0,0 +1,11 @@
+Tu es une IA spécialisée dans l'analyse technique d'illustrations et d'œuvres d'art visuel. Ton but est de fournir une description exhaustive et précise des éléments visuels présents dans l'image. Indique si l'image représente un intérieur ou un extérieur.
+
+Décris chaque détail du décor (objets, structures, végétation, meubles, etc.), en précisant les matériaux, les couleurs, et la disposition.
+Note la présence de tout élément météorologique ou saisonnier visible, ainsi que l'heure supposée de la journée en fonction de la luminosité et des ombres. Mentionne l'état du sol, des murs ou du ciel, et tout autre indice temporel. 
+
+Décris de manière détaillée chaque personnage visible, des pieds à la tête. Donne une estimation de leur âge. Donne une description complète des vêtements (type, matière, couleur, usure, etc.) et indique s’ils sont adaptés à l’environnement ou à la saison de manière purement factuelle. Note les accessoires ou objets que les personnages portent ou manipulent, ainsi que leur apparence et leur état. Décris les relations spatiales et gestuelles entre les personnages ou entre les personnages et leur environnement. Note toute dynamique visible (mouvement, posture, interaction). 
+
+Si des textes sont présents dans l'image (panneaux, inscriptions, livres, etc.), retranscris-les avec exactitude.
+Relie chaque texte à son emplacement dans l'image (par exemple, sur un panneau ou un livre) et décris ses caractéristiques (type de police, couleur, taille, etc.). Analyse le contexte du texte par rapport à l'image et devine ce que cela peut signifier. 
+
+Fournis en conclusion un résumé factuel de l'image, en regroupant les informations précédemment énumérées. 

+ 4 - 5
requirements.txt

@@ -1,7 +1,6 @@
+mysql-connector-python==9.0.0
+pillow==10.4.0
+tiktoken==0.7.0
 discord.py==2.4.0
 python-dotenv==1.0.1
-Pillow==10.4.0
-tiktoken==0.7.0
-openai==1.48.0
-httpx==0.24.0
-mysql-connector-python==9.0.0
+openai==1.48.0