1
0
Johnounet 2 сар өмнө
parent
commit
ac2e15d32c
1 өөрчлөгдсөн 168 нэмэгдсэн , 210 устгасан
  1. 168 210
      chatbot.py

+ 168 - 210
chatbot.py

@@ -6,6 +6,7 @@ import os
 import random
 import re
 from dotenv import load_dotenv
+from datetime import datetime
 import logging
 from logging.handlers import RotatingFileHandler
 
@@ -13,13 +14,9 @@ from logging.handlers import RotatingFileHandler
 logger = logging.getLogger('discord_bot')
 logger.setLevel(logging.INFO)
 formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
-
-# Créer un gestionnaire de fichier avec rotation
-file_handler = RotatingFileHandler('bot.log', maxBytes=5*1024*1024, backupCount=2)  # 5 Mo par fichier, garder 2 sauvegardes
+file_handler = RotatingFileHandler('bot.log', maxBytes=5*1024*1024, backupCount=2)
 file_handler.setFormatter(formatter)
 logger.addHandler(file_handler)
-
-# Optionnel : ajouter un gestionnaire de console pour afficher les logs dans la console
 console_handler = logging.StreamHandler()
 console_handler.setFormatter(formatter)
 logger.addHandler(console_handler)
@@ -28,9 +25,8 @@ logger.addHandler(console_handler)
 load_dotenv()
 
 # Version du bot
-VERSION = "4.5.0"  # Modifiable selon la version actuelle
+VERSION = "4.6.0"
 
-# Récupérer les variables d'environnement avec validation
 def get_env_variable(var_name, is_critical=True, default=None, var_type=str):
     value = os.getenv(var_name)
     if value is None:
@@ -56,46 +52,64 @@ def get_env_variable(var_name, is_critical=True, default=None, var_type=str):
     return value
 
 try:
-    # Variables d'environnement critiques
     MISTRAL_API_KEY = get_env_variable('MISTRAL_API_KEY')
     DISCORD_TOKEN = get_env_variable('DISCORD_TOKEN')
     CHANNEL_ID = get_env_variable('CHANNEL_ID', var_type=int)
-
-    # Variables d'environnement non critiques avec valeurs par défaut
     MAX_HISTORY_LENGTH = get_env_variable('MAX_HISTORY_LENGTH', is_critical=False, default=10, var_type=int)
     HISTORY_FILE = get_env_variable('HISTORY_FILE', is_critical=False, default="conversation_history.json")
-
     logger.info("Toutes les variables d'environnement critiques ont été chargées avec succès.")
 except ValueError as e:
     logger.error(f"Erreur lors du chargement des variables d'environnement: {e}")
-    # Si une variable critique est manquante, le bot ne peut pas fonctionner correctement.
-    # Il est donc préférable de quitter le programme avec un code d'erreur.
     exit(1)
 
-# Endpoint API Mistral
 MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions"
 
-def load_history():
-    """Charge l'historique depuis un fichier JSON."""
-    if os.path.exists(HISTORY_FILE):
-        with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
+class ConversationHistory:
+    def __init__(self, file_path, max_length):
+        self.file_path = file_path
+        self.max_length = max_length
+        self.history = self.load_history()
+
+    def load_history(self):
+        if os.path.exists(self.file_path):
             try:
-                data = json.load(f)
-                # Vérifier et limiter la taille de chaque historique
-                for channel_id in data:
-                    if "messages" in data[channel_id]:
-                        if len(data[channel_id]["messages"]) > MAX_HISTORY_LENGTH:
-                            data[channel_id]["messages"] = data[channel_id]["messages"][-MAX_HISTORY_LENGTH:]
-                return data
+                with open(self.file_path, 'r', encoding='utf-8') as f:
+                    data = json.load(f)
+                    for channel_id in data:
+                        if "messages" in data[channel_id]:
+                            if len(data[channel_id]["messages"]) > self.max_length:
+                                data[channel_id]["messages"] = data[channel_id]["messages"][-self.max_length:]
+                    return data
             except json.JSONDecodeError:
                 logger.error("Erreur de lecture du fichier d'historique. Création d'un nouveau fichier.")
                 return {}
-    return {}
+        return {}
+
+    def save_history(self):
+        with open(self.file_path, 'w', encoding='utf-8') as f:
+            json.dump(self.history, f, ensure_ascii=False, indent=4)
+
+    def add_message(self, channel_id, message):
+        if channel_id not in self.history:
+            self.history[channel_id] = {"messages": []}
+        self.history[channel_id]["messages"].append(message)
+        if len(self.history[channel_id]["messages"]) > self.max_length:
+            self.history[channel_id]["messages"] = self.history[channel_id]["messages"][-self.max_length:]
+        self.save_history()
+
+    def get_history(self, channel_id):
+        if channel_id in self.history:
+            return self.history[channel_id]
+        else:
+            self.history[channel_id] = {"messages": []}
+            return self.history[channel_id]
 
-def save_history(history):
-    """Sauvegarde l'historique dans un fichier JSON."""
-    with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
-        json.dump(history, f, ensure_ascii=False, indent=4)
+    def reset_history(self, channel_id):
+        if channel_id in self.history:
+            self.history[channel_id]["messages"] = []
+        else:
+            self.history[channel_id] = {"messages": []}
+        self.save_history()
 
 def get_personality_prompt():
     try:
@@ -109,36 +123,7 @@ def get_personality_prompt():
         Lorsque tu analyses une image, décris d'abord ce que tu vois en détail,
         puis réponds à la question si elle est posée. Utilise un langage clair et accessible."""
 
-# Charger l'historique au démarrage
-conversation_history = load_history()
-
-intents = discord.Intents.default()
-intents.messages = True
-intents.message_content = True
-intents.presences = True
-
-bot = commands.Bot(command_prefix='!', intents=intents)
-
-@bot.event
-async def on_ready():
-    logger.info(f'Le bot est connecté en tant que {bot.user}')
-    global conversation_history
-    conversation_history = load_history()
-    channel = bot.get_channel(CHANNEL_ID)
-    if channel is not None:
-        guild = channel.guild
-        if guild is not None:
-            bot_member = guild.me
-            bot_nickname = bot_member.display_name
-        else:
-            bot_nickname = bot.user.name
-        embed = discord.Embed(
-            title="Bot en ligne",
-            description=f"{bot_nickname} est désormais en ligne. Version {VERSION}.",
-            color=discord.Color.green()
-        )
-        await channel.send(embed=embed)
-    await bot.tree.sync()  # Synchroniser les commandes slash
+history_manager = ConversationHistory(HISTORY_FILE, MAX_HISTORY_LENGTH)
 
 def call_mistral_api(prompt, history, image_url=None, user_id=None, username=None):
     headers = {
@@ -146,71 +131,61 @@ def call_mistral_api(prompt, history, image_url=None, user_id=None, username=Non
         "Authorization": f"Bearer {MISTRAL_API_KEY}"
     }
     personality_prompt = get_personality_prompt()
-    # Vérifier si la structure messages existe
-    if "messages" not in history:
-        history["messages"] = []
-    # Création du message utilisateur selon qu'il y a une image ou non
+    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
     if image_url:
-        # Format multimodal pour les messages avec image
         user_content = [
-            {"type": "text", "text": f"{username}: {prompt}" if username else prompt},
+            {"type": "text", "text": f"{username}: {prompt} (Date et heure : {current_time})" if username else f"{prompt} (Date et heure : {current_time})"},
             {
                 "type": "image_url",
                 "image_url": {
                     "url": image_url,
-                    "detail": "high"  # Demander une analyse détaillée de l'image
+                    "detail": "high"
                 }
             }
         ]
-        user_message = {
-            "role": "user",
-            "content": user_content
-        }
+        user_message = {"role": "user", "content": user_content}
     else:
-        # Format standard pour les messages texte seulement
-        user_content = f"{username}: {prompt}" if username else prompt
+        user_content = f"{username}: {prompt} (Date et heure : {current_time})" if username else f"{prompt} (Date et heure : {current_time})"
         user_message = {"role": "user", "content": user_content}
-    # Ajouter le message utilisateur à l'historique
+
     history["messages"].append(user_message)
-    # Limiter l'historique à MAX_HISTORY_LENGTH messages
     if len(history["messages"]) > MAX_HISTORY_LENGTH:
         history["messages"] = history["messages"][-MAX_HISTORY_LENGTH:]
-    # Préparer les messages pour l'API
-    messages = []
-    # Ajouter le message système en premier
-    messages.append({"role": "system", "content": personality_prompt})
-    # Ajouter l'historique des messages (en gardant le format)
+
+    messages = [{"role": "system", "content": personality_prompt}]
     for msg in history["messages"]:
-        if isinstance(msg["content"], list):  # C'est un message multimodal
-            messages.append({
-                "role": msg["role"],
-                "content": msg["content"]
-            })
-        else:  # C'est un message texte standard
-            messages.append({
-                "role": msg["role"],
-                "content": msg["content"]
-            })
+        messages.append({
+            "role": msg["role"],
+            "content": msg["content"] if isinstance(msg["content"], list) else msg["content"]
+        })
+
     data = {
-        "model": "mistral-medium-2508",
+        "model": "mistral-medium-latest",
         "messages": messages,
         "max_tokens": 1000
     }
+
     try:
         response = requests.post(MISTRAL_API_URL, headers=headers, data=json.dumps(data))
-        response.raise_for_status()  # Lève une exception pour les erreurs HTTP
+        response.raise_for_status()
         if response.status_code == 200:
             response_data = response.json()
-            # Vérifier si la réponse contient bien le champ attendu
             if 'choices' in response_data and len(response_data['choices']) > 0:
                 assistant_response = response_data['choices'][0]['message']['content']
-                # Ajouter la réponse de l'assistant à l'historique
                 history["messages"].append({"role": "assistant", "content": assistant_response})
-                # Limiter à nouveau après avoir ajouté la réponse
+                if 'usage' in response_data:
+                    prompt_tokens = response_data['usage']['prompt_tokens']
+                    completion_tokens = response_data['usage']['completion_tokens']
+                    input_cost = (prompt_tokens / 1_000_000) * 0.4
+                    output_cost = (completion_tokens / 1_000_000) * 2
+                    total_cost = input_cost + output_cost
+                    logger.info(f"Mistral API Call - Input Tokens: {prompt_tokens}, Output Tokens: {completion_tokens}, Cost: ${total_cost:.6f}")
+                else:
+                    logger.warning("La réponse de l'API ne contient pas d'informations sur les tokens.")
                 if len(history["messages"]) > MAX_HISTORY_LENGTH:
                     history["messages"] = history["messages"][-MAX_HISTORY_LENGTH:]
-                # Sauvegarder l'historique après chaque modification
-                save_history(conversation_history)
+                history_manager.save_history()
                 return assistant_response
             else:
                 logger.error(f"Réponse API inattendue: {response_data}")
@@ -221,97 +196,39 @@ def call_mistral_api(prompt, history, image_url=None, user_id=None, username=Non
         logger.error(f"Erreur lors de l'appel API: {e}")
         return "Désolé, une erreur réseau est survenue lors de la communication avec l'API."
 
-@bot.tree.command(name="reset", description="Réinitialise l'historique de conversation")
-async def reset_history_slash(interaction: discord.Interaction):
-    channel_id = str(interaction.channel.id)
-    if channel_id in conversation_history:
-        # Conserver le même ID de conversation mais vider les messages
-        if "messages" in conversation_history[channel_id]:
-            conversation_history[channel_id]["messages"] = []
-        else:
-            conversation_history[channel_id] = {
-                "conversation_id": conversation_history[channel_id].get("conversation_id", str(len(conversation_history) + 1)),
-                "messages": []
-            }
-        save_history(conversation_history)
-        await interaction.response.send_message("L'historique de conversation a été réinitialisé.")
-    else:
-        conversation_id = str(len(conversation_history) + 1)
-        conversation_history[channel_id] = {
-            "conversation_id": conversation_id,
-            "messages": []
-        }
-        save_history(conversation_history)
-        await interaction.response.send_message("Aucun historique de conversation trouvé pour ce channel. Créé un nouvel historique.")
+intents = discord.Intents.default()
+intents.messages = True
+intents.message_content = True
+intents.presences = True
+bot = commands.Bot(command_prefix='!', intents=intents)
 
 @bot.event
-async def on_message(message):
-    # Ignorer les messages du bot lui-même
-    if message.author == bot.user:
-        return
-
-    # Vérifier si le bot est mentionné dans le message
-    if bot.user.mentioned_in(message):
-        # Vérifier si le message provient du channel spécifique
-        if message.channel.id == CHANNEL_ID:
-            # Traiter comme avant (ignorer pour l'instant, car nous voulons que la nouvelle fonctionnalité s'applique partout sauf dans CHANNEL_ID)
-            pass
+async def on_ready():
+    logger.info(f'Le bot est connecté en tant que {bot.user}')
+    history_manager.history = history_manager.load_history()
+    channel = bot.get_channel(CHANNEL_ID)
+    if channel is not None:
+        guild = channel.guild
+        if guild is not None:
+            bot_member = guild.me
+            bot_nickname = bot_member.display_name
         else:
-            # Récupérer les vingt derniers messages dans ce canal (sans compter le message actuel)
-            context_messages = []
-            async for msg in message.channel.history(limit=20, before=message):
-                # Remplacer les mentions par les noms d'utilisateur pour éviter les références circulaires
-                resolved_content = msg.content
-                for user in msg.mentions:
-                    resolved_content = resolved_content.replace(f"<@{user.id}>", f"@{user.display_name}")
-                # Ajouter le nom de l'auteur avant le contenu du message
-                author_name = msg.author.display_name
-                context_messages.append(f"{author_name}: {resolved_content}")
-            # Inverser l'ordre pour avoir les messages du plus ancien au plus récent
-            context_messages.reverse()
-            # Construire le contexte
-            context = "\n".join(context_messages)
-            # Préparer le prompt avec le contexte
-            # Remplacer les mentions dans le message actuel
-            resolved_content = message.content
-            for user in message.mentions:
-                resolved_content = resolved_content.replace(f"<@{user.id}>", f"@{user.display_name}")
-            # Supprimer la mention du bot du message pour éviter les répétitions
-            bot_mention = f"<@{bot.user.id}>"
-            if bot_mention in resolved_content:
-                resolved_content = resolved_content.replace(bot_mention, "").strip()
-            prompt = f"Contexte de la conversation récente:\n{context}\n\nNouveau message: {resolved_content}"
-            # Utiliser le prompt pour appeler l'API Mistral
-            channel_id = str(message.channel.id)
-            # Créer un historique temporaire pour cette conversation
-            temp_history = {
-                "messages": [
-                    {"role": "system", "content": get_personality_prompt()},
-                    {"role": "user", "content": prompt}
-                ]
-            }
-            # Appeler l'API Mistral
-            async with message.channel.typing():
-                try:
-                    response = call_mistral_api(
-                        prompt,
-                        temp_history,  # Utiliser l'historique temporaire
-                        None,  # Pas d'image ici
-                        user_id=str(message.author.id),
-                        username=message.author.display_name
-                    )
-                    await message.channel.send(response)
-                except Exception as e:
-                    logger.error(f"Erreur lors de l'appel à l'API: {e}")
-                    await message.channel.send("Désolé, une erreur est survenue lors du traitement de votre demande.")
-            return
+            bot_nickname = bot.user.name
+        embed = discord.Embed(
+            title="Bot en ligne",
+            description=f"{bot_nickname} est désormais en ligne. Version {VERSION}.",
+            color=discord.Color.green()
+        )
+        await channel.send(embed=embed)
+    await bot.tree.sync()
 
-    # Vérifier si le message provient du channel spécifique
-    if message.channel.id != CHANNEL_ID:
-        return
+@bot.tree.command(name="reset", description="Réinitialise l'historique de conversation")
+async def reset_history_slash(interaction: discord.Interaction):
+    channel_id = str(interaction.channel.id)
+    history_manager.reset_history(channel_id)
+    await interaction.response.send_message("L'historique de conversation a été réinitialisé.")
 
-    # Le reste de la fonction on_message pour le traitement normal dans le canal spécifique
-    # Gestion des stickers (code existant)
+async def handle_stickers(message):
     if message.stickers:
         guild = message.guild
         if guild:
@@ -333,9 +250,10 @@ async def on_message(message):
                 await message.channel.send("Aucun sticker personnalisé trouvé sur ce serveur.")
         else:
             await message.channel.send("Ce message ne provient pas d'un serveur.")
-        return
+        return True
+    return False
 
-    # Gestion des emojis personnalisés (code existant)
+async def handle_emojis(message):
     emoji_pattern = re.compile(r'^<a?:\w+:\d+>$')
     content = message.content.strip()
     if emoji_pattern.match(content):
@@ -344,15 +262,16 @@ async def on_message(message):
             random_emoji = random.choice(guild.emojis)
             try:
                 await message.channel.send(str(random_emoji))
-                return
+                return True
             except discord.errors.Forbidden as e:
                 logger.error(f"Erreur lors de l'envoi de l'emoji: {random_emoji.name} (ID: {random_emoji.id}). Erreur: {e}")
                 await message.channel.send("Je n'ai pas pu envoyer d'emoji en réponse.")
         else:
             await message.channel.send("Aucun emoji personnalisé trouvé sur ce serveur.")
-        return
+        return True
+    return False
 
-    # Traitement des images et autres fonctionnalités (code existant)
+async def handle_images(message):
     if message.attachments:
         image_count = 0
         non_image_files = []
@@ -375,48 +294,89 @@ async def on_message(message):
         if non_image_files:
             file_list = ", ".join(non_image_files)
             await message.channel.send(f"Erreur : Les fichiers suivants ne sont pas des images et ne sont pas pris en charge : {file_list}. Veuillez envoyer uniquement des images.")
-            return
+            return False
         if image_count > 1:
             await message.channel.send("Erreur : Vous ne pouvez pas envoyer plus d'une image en un seul message.")
-            return
+            return False
         if too_large_images:
             image_list = ", ".join(too_large_images)
             await message.channel.send(f"Erreur : Les images suivantes dépassent la limite de 5 Mo : {image_list}. Veuillez envoyer des images plus petites.")
-            return
+            return False
+    return True
 
-    # Récupérer ou initialiser l'historique pour ce channel
+async def handle_bot_mention(message):
+    context_messages = []
+    async for msg in message.channel.history(limit=20, before=message):
+        resolved_content = msg.content
+        for user in msg.mentions:
+            resolved_content = resolved_content.replace(f"<@{user.id}>", f"@{user.display_name}")
+        author_name = msg.author.display_name
+        context_messages.append(f"{author_name}: {resolved_content}")
+    context_messages.reverse()
+    context = "\n".join(context_messages)
+    resolved_content = message.content
+    for user in message.mentions:
+        resolved_content = resolved_content.replace(f"<@{user.id}>", f"@{user.display_name}")
+    bot_mention = f"<@{bot.user.id}>"
+    if bot_mention in resolved_content:
+        resolved_content = resolved_content.replace(bot_mention, "").strip()
+    prompt = f"Contexte de la conversation récente:\n{context}\n\nNouveau message: {resolved_content}"
     channel_id = str(message.channel.id)
-    global conversation_history
-    conversation_history = load_history()
-    if channel_id not in conversation_history:
-        conversation_id = str(len(conversation_history) + 1)
-        conversation_history[channel_id] = {
-            "conversation_id": conversation_id,
-            "messages": []
-        }
-    if "messages" not in conversation_history[channel_id]:
-        conversation_history[channel_id]["messages"] = []
+    temp_history = {
+        "messages": [
+            {"role": "system", "content": get_personality_prompt()},
+            {"role": "user", "content": prompt}
+        ]
+    }
+    async with message.channel.typing():
+        try:
+            response = call_mistral_api(
+                prompt,
+                temp_history,
+                None,
+                user_id=str(message.author.id),
+                username=message.author.display_name
+            )
+            await message.channel.send(response)
+        except Exception as e:
+            logger.error(f"Erreur lors de l'appel à l'API: {e}")
+            await message.channel.send("Désolé, une erreur est survenue lors du traitement de votre demande.")
 
-    # Traitement des images dans le message (code existant)
+@bot.event
+async def on_message(message):
+    if message.author == bot.user:
+        return
+    if bot.user.mentioned_in(message):
+        if message.channel.id == CHANNEL_ID:
+            pass
+        else:
+            await handle_bot_mention(message)
+            return
+    if message.channel.id != CHANNEL_ID:
+        return
+    if await handle_stickers(message):
+        return
+    if await handle_emojis(message):
+        return
+    if not await handle_images(message):
+        return
+    channel_id = str(message.channel.id)
+    history = history_manager.get_history(channel_id)
     image_url = None
     if message.attachments:
         for attachment in message.attachments:
             if attachment.content_type and attachment.content_type.startswith('image/'):
                 image_url = attachment.url
                 break
-
-    # Utiliser le contenu résolu (avec les mentions remplacées)
     resolved_content = message.content
     for user in message.mentions:
         resolved_content = resolved_content.replace(f"<@{user.id}>", f"@{user.display_name}")
     prompt = resolved_content
-
-    # Appeler l'API Mistral (code existant)
     async with message.channel.typing():
         try:
             response = call_mistral_api(
                 prompt,
-                conversation_history[channel_id],
+                history,
                 image_url,
                 user_id=str(message.author.id),
                 username=message.author.display_name
@@ -425,8 +385,6 @@ async def on_message(message):
         except Exception as e:
             logger.error(f"Erreur lors de l'appel à l'API: {e}")
             await message.channel.send("Désolé, une erreur est survenue lors du traitement de votre demande.")
-
-    # Assurer que les autres gestionnaires d'événements reçoivent également le message
     await bot.process_commands(message)
 
 bot.run(DISCORD_TOKEN)