1
0
Johnounet 1 жил өмнө
parent
commit
aa8bb945af
2 өөрчлөгдсөн 240 нэмэгдсэн , 21 устгасан
  1. 239 21
      chatbot.py
  2. 1 0
      requirements.txt

+ 239 - 21
chatbot.py

@@ -6,11 +6,14 @@ from io import BytesIO
 import asyncio
 import asyncio
 
 
 import mysql.connector
 import mysql.connector
+import pytz
 from mysql.connector import Error
 from mysql.connector import Error
 from PIL import Image
 from PIL import Image
 import tiktoken
 import tiktoken
 import discord
 import discord
 from discord.ext import commands
 from discord.ext import commands
+from discord import app_commands
+from datetime import datetime, timezone
 from dotenv import load_dotenv
 from dotenv import load_dotenv
 from openai import AsyncOpenAI, OpenAIError
 from openai import AsyncOpenAI, OpenAIError
 
 
@@ -26,7 +29,7 @@ DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
 PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
 PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
 IMAGE_ANALYSIS_PROMPT_FILE = os.getenv('IMAGE_ANALYSIS_PROMPT_FILE', 'image_analysis_prompt.txt')
 IMAGE_ANALYSIS_PROMPT_FILE = os.getenv('IMAGE_ANALYSIS_PROMPT_FILE', 'image_analysis_prompt.txt')
 BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
 BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
-BOT_VERSION = "2.7.0"
+BOT_VERSION = "2.8.0"
 
 
 # Validation des variables d'environnement
 # Validation des variables d'environnement
 required_env_vars = {
 required_env_vars = {
@@ -70,6 +73,7 @@ logging.getLogger('httpx').setLevel(logging.WARNING)  # Réduire le niveau de lo
 # Initialiser les intents Discord
 # Initialiser les intents Discord
 intents = discord.Intents.default()
 intents = discord.Intents.default()
 intents.message_content = True
 intents.message_content = True
+intents.members = True
 
 
 # Initialiser le client OpenAI asynchrone
 # Initialiser le client OpenAI asynchrone
 openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
 openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
@@ -162,6 +166,40 @@ class DatabaseManager:
             self.connection.close()
             self.connection.close()
             logger.info("Connexion à la base de données fermée.")
             logger.info("Connexion à la base de données fermée.")
 
 
+    def add_reminder(self, user_id, channel_id, remind_at, content):
+        try:
+            with self.connection.cursor() as cursor:
+                sql = """
+                INSERT INTO reminders (user_id, channel_id, remind_at, content)
+                VALUES (%s, %s, %s, %s)
+                """
+                cursor.execute(sql, (user_id, channel_id, remind_at, content))
+            self.connection.commit()
+            logger.info(f"Rappel ajouté pour l'utilisateur {user_id} à {remind_at}")
+        except Error as e:
+            logger.error(f"Erreur lors de l'ajout du rappel: {e}")
+
+    def get_due_reminders(self, current_time):
+        try:
+            with self.connection.cursor(dictionary=True) as cursor:
+                sql = "SELECT * FROM reminders WHERE remind_at <= %s"
+                cursor.execute(sql, (current_time,))
+                reminders = cursor.fetchall()
+            return reminders
+        except Error as e:
+            logger.error(f"Erreur lors de la récupération des rappels: {e}")
+            return []
+
+    def delete_reminder(self, reminder_id):
+        try:
+            with self.connection.cursor() as cursor:
+                sql = "DELETE FROM reminders WHERE id = %s"
+                cursor.execute(sql, (reminder_id,))
+            self.connection.commit()
+            logger.info(f"Rappel ID {reminder_id} supprimé")
+        except Error as e:
+            logger.error(f"Erreur lors de la suppression du rappel ID {reminder_id}: {e}")
+
 # ===============================
 # ===============================
 # Gestion de l'Historique des Messages
 # Gestion de l'Historique des Messages
 # ===============================
 # ===============================
@@ -174,6 +212,26 @@ messages_since_last_analysis = 0
 # Fonctions Utilitaires
 # Fonctions Utilitaires
 # ====================
 # ====================
 
 
+def split_message(message, max_length=2000):
+    """Divise un message en plusieurs segments de longueur maximale spécifiée."""
+    if len(message) <= max_length:
+        return [message]
+    
+    parts = []
+    current_part = ""
+    
+    for line in message.split('\n'):
+        if len(current_part) + len(line) + 1 > max_length:
+            parts.append(current_part)
+            current_part = line + '\n'
+        else:
+            current_part += line + '\n'
+    
+    if current_part:
+        parts.append(current_part)
+    
+    return parts
+
 def has_text(text):
 def has_text(text):
     """Détermine si le texte fourni est non vide après suppression des espaces."""
     """Détermine si le texte fourni est non vide après suppression des espaces."""
     return bool(text.strip())
     return bool(text.strip())
@@ -368,7 +426,7 @@ async def call_gpt4o_mini_with_analysis(analysis_text, user_name, user_question,
     reply = await call_openai_model(
     reply = await call_openai_model(
         model="gpt-4o-mini",
         model="gpt-4o-mini",
         messages=messages,
         messages=messages,
-        max_tokens=450,
+        max_tokens=4096,
         temperature=1.0
         temperature=1.0
     )
     )
     
     
@@ -403,7 +461,7 @@ async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
     reply = await call_openai_model(
     reply = await call_openai_model(
         model="gpt-4o-mini",
         model="gpt-4o-mini",
         messages=messages,
         messages=messages,
-        max_tokens=450,
+        max_tokens=4096,
         temperature=1.0
         temperature=1.0
     )
     )
     
     
@@ -469,23 +527,159 @@ async def add_to_conversation_history(db_manager, new_message):
 # Gestion des Événements Discord
 # Gestion des Événements Discord
 # =====================================
 # =====================================
 
 
-class MyDiscordClient(discord.Client):
-    def __init__(self, db_manager, **options):
-        super().__init__(**options)
+class ReminderCommands(commands.Cog):
+    """Cog pour les commandes de rappel."""
+
+    def __init__(self, bot: commands.Bot, db_manager: DatabaseManager):
+        self.bot = bot
+        self.db_manager = db_manager
+
+    @app_commands.command(name="rappel", description="Créer un rappel")
+    @app_commands.describe(date="Date du rappel (DD/MM/YYYY)")
+    @app_commands.describe(time="Heure du rappel (HH:MM, 24h)")
+    @app_commands.describe(content="Contenu du rappel")
+    async def rappel(self, interaction: discord.Interaction, date: str, time: str, content: str):
+        """Commande pour créer un rappel."""
+        user = interaction.user
+        channel = interaction.channel
+
+        # Valider et parser la date et l'heure
+        try:
+            remind_datetime_str = f"{date} {time}"
+            remind_datetime = datetime.strptime(remind_datetime_str, "%d/%m/%Y %H:%M")
+            # Vous pouvez ajuster le fuseau horaire selon vos besoins
+            tz = pytz.timezone('Europe/Paris')  # Exemple de fuseau horaire
+            remind_datetime = tz.localize(remind_datetime)
+            now = datetime.now(tz)
+            if remind_datetime <= now:
+                await interaction.response.send_message("❌ La date et l'heure doivent être dans le futur.", ephemeral=True)
+                return
+        except ValueError:
+            await interaction.response.send_message("❌ Format de date ou d'heure invalide. Utilisez DD/MM/YYYY pour la date et HH:MM pour l'heure.", ephemeral=True)
+            return
+
+        # Ajouter le rappel à la base de données
+        self.db_manager.add_reminder(
+            user_id=str(user.id),
+            channel_id=str(channel.id),
+            remind_at=remind_datetime.strftime('%Y-%m-%d %H:%M:%S'),
+            content=content
+        )
+
+        # Créer un embed pour la confirmation
+        embed = discord.Embed(
+            title="Rappel Créé ✅",
+            description=(
+                f"**Date et Heure** : {remind_datetime.strftime('%d/%m/%Y %H:%M')}\n"
+                f"**Contenu** : {content}"
+            ),
+            color=0x00ff00,  # Vert
+            timestamp=datetime.now(timezone.utc)
+        )
+        embed.set_footer(text=f"Créé par {user}", icon_url=user.display_avatar.url if user.avatar else user.default_avatar.url)
+
+        # Envoyer l'embed de confirmation
+        await interaction.response.send_message(embed=embed)
+
+    @rappel.error
+    async def rappel_error(self, interaction: discord.Interaction, error):
+        """Gère les erreurs de la commande rappel."""
+        logger.error(f"Erreur lors de l'exécution de la commande rappel: {error}")
+        await interaction.response.send_message("❌ Une erreur est survenue lors de la création du rappel.")
+
+class MyDiscordBot(commands.Bot):
+    def __init__(self, db_manager, **kwargs):
+        super().__init__(**kwargs)
         self.db_manager = db_manager
         self.db_manager = db_manager
         self.message_queue = asyncio.Queue()
         self.message_queue = asyncio.Queue()
+        self.reminder_task = None
 
 
     async def setup_hook(self):
     async def setup_hook(self):
         """Hook d'initialisation asynchrone pour configurer des tâches supplémentaires."""
         """Hook d'initialisation asynchrone pour configurer des tâches supplémentaires."""
         self.processing_task = asyncio.create_task(self.process_messages())
         self.processing_task = asyncio.create_task(self.process_messages())
+        self.reminder_task = asyncio.create_task(self.process_reminders())
+        # Charger les commandes slash
+        await self.add_cog(AdminCommands(self, self.db_manager))
+        await self.add_cog(ReminderCommands(self, self.db_manager))
+        await self.tree.sync()  # Synchroniser les commandes slash
 
 
     async def close(self):
     async def close(self):
         if openai_client:
         if openai_client:
             await openai_client.close()
             await openai_client.close()
         self.db_manager.close_connection()
         self.db_manager.close_connection()
         self.processing_task.cancel()
         self.processing_task.cancel()
+        if self.reminder_task:
+            self.reminder_task.cancel()
         await super().close()
         await super().close()
 
 
+    async def process_reminders(self):
+        """Tâche en arrière-plan pour vérifier et envoyer les rappels."""
+        await self.wait_until_ready()
+        while not self.is_closed():
+            try:
+                now = datetime.now(pytz.timezone('Europe/Paris'))  # Utiliser le même fuseau horaire
+                reminders = self.db_manager.get_due_reminders(now.strftime('%Y-%m-%d %H:%M:%S'))
+                for reminder in reminders:
+                    try:
+                        user = await self.fetch_user(int(reminder['user_id']))
+                    except discord.NotFound:
+                        logger.error(f"Utilisateur avec l'ID {reminder['user_id']} non trouvé.")
+                        continue
+                    except discord.HTTPException as e:
+                        logger.error(f"Erreur lors de la récupération de l'utilisateur {reminder['user_id']}: {e}")
+                        continue
+
+                    try:
+                        channel = await self.fetch_channel(int(reminder['channel_id']))
+                    except discord.NotFound:
+                        logger.error(f"Canal avec l'ID {reminder['channel_id']} non trouvé.")
+                        continue
+                    except discord.HTTPException as e:
+                        logger.error(f"Erreur lors de la récupération du canal {reminder['channel_id']}: {e}")
+                        continue
+
+                    if channel and user:
+                        personalized_content = await self.get_personalized_reminder(reminder['content'], user)
+                        try:
+                            reminder_message = f"{user.mention} 🕒 Rappel : {personalized_content}"
+                            await channel.send(reminder_message)
+                            logger.info(f"Rappel envoyé à {user} dans le canal {channel}.")
+
+                            self.db_manager.save_message('assistant', reminder_message)
+
+                            conversation_history.append({
+                                "role": "assistant",
+                                "content": reminder_message
+                            })
+
+                        except discord.Forbidden:
+                            logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal {channel}.")
+                        except discord.HTTPException as e:
+                            logger.error(f"Erreur lors de l'envoi du message dans le canal {channel}: {e}")
+                    else:
+                        logger.warning(f"Canal ou utilisateur introuvable pour le rappel ID {reminder['id']}.")
+
+                    # Supprimer le rappel après envoi
+                    self.db_manager.delete_reminder(reminder['id'])
+                await asyncio.sleep(60)  # Vérifier toutes les minutes
+            except Exception as e:
+                logger.error(f"Erreur dans la tâche de rappels: {e}")
+                await asyncio.sleep(60)
+
+    async def get_personalized_reminder(self, content, user):
+        """Utilise l'API OpenAI pour personnaliser le contenu du rappel."""
+        messages = [
+            {"role": "system", "content": PERSONALITY_PROMPT},
+            {"role": "user", "content": f"Personnalise le rappel suivant pour {user.name} : {content}"}
+        ]
+        reply = await call_openai_model(
+            model="gpt-4o-mini",
+            messages=messages,
+            max_tokens=4096,
+            temperature=1.0
+        )
+        return reply if reply else content
+
     async def on_ready(self):
     async def on_ready(self):
         """Événement déclenché lorsque le bot est prêt."""
         """Événement déclenché lorsque le bot est prêt."""
         logger.info(f'{BOT_NAME} connecté en tant que {self.user}')
         logger.info(f'{BOT_NAME} connecté en tant que {self.user}')
@@ -541,17 +735,6 @@ class MyDiscordClient(discord.Client):
 
 
         user_text = message.content.strip()
         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é.")
-            return
-
         # Traiter les pièces jointes
         # Traiter les pièces jointes
         image_data = None
         image_data = None
         file_content = None
         file_content = None
@@ -635,7 +818,10 @@ class MyDiscordClient(discord.Client):
                 # Appeler l'API OpenAI pour le texte
                 # Appeler l'API OpenAI pour le texte
                 reply = await call_openai_api(user_text, message.author.name)
                 reply = await call_openai_api(user_text, message.author.name)
                 if reply:
                 if reply:
-                    await message.channel.send(reply)
+                    # Diviser le message en plusieurs parties si nécessaire
+                    message_parts = split_message(reply)
+                    for part in message_parts:
+                        await message.channel.send(part)
 
 
                     # Construire et ajouter les messages à l'historique
                     # Construire et ajouter les messages à l'historique
                     user_message = {
                     user_message = {
@@ -656,6 +842,35 @@ class MyDiscordClient(discord.Client):
                 await message.channel.send("Une erreur est survenue lors de la génération de la réponse.")
                 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}")
                 logger.error(f"Erreur lors du traitement du texte: {e}")
 
 
+# ============================
+# Commandes Slash via Cogs
+# ============================
+
+class AdminCommands(commands.Cog):
+    """Cog pour les commandes administratives."""
+
+    def __init__(self, bot: commands.Bot, db_manager):
+        self.bot = bot
+        self.db_manager = db_manager
+
+    @app_commands.command(name="reset_history", description="Réinitialise l'historique des conversations.")
+    @app_commands.checks.has_permissions(administrator=True)
+    async def reset_history(self, interaction: discord.Interaction):
+        """Réinitialise l'historique des conversations."""
+        global conversation_history
+        conversation_history = []
+        self.db_manager.reset_history()
+        await interaction.response.send_message("✅ L'historique des conversations a été réinitialisé.")
+
+    @reset_history.error
+    async def reset_history_error(self, interaction: discord.Interaction, error):
+        """Gère les erreurs de la commande reset_history."""
+        if isinstance(error, app_commands.CheckFailure):
+            await interaction.response.send_message("❌ Vous n'avez pas la permission d'utiliser cette commande.")
+        else:
+            logger.error(f"Erreur lors de l'exécution de la commande reset_history: {error}")
+            await interaction.response.send_message("Une erreur est survenue lors de l'exécution de la commande.")
+
 # ============================
 # ============================
 # Démarrage du Bot Discord
 # Démarrage du Bot Discord
 # ============================
 # ============================
@@ -668,13 +883,16 @@ def main():
 
 
     db_manager.load_conversation_history()
     db_manager.load_conversation_history()
 
 
-    client_discord = MyDiscordClient(db_manager=db_manager, intents=intents)
+    # Initialiser le bot avec le préfixe "!" et les intents définis
+    bot = MyDiscordBot(command_prefix="!", db_manager=db_manager, intents=intents)
+
+    # Démarrer le bot
     try:
     try:
-        client_discord.run(DISCORD_TOKEN)
+        bot.run(DISCORD_TOKEN)
     except Exception as e:
     except Exception as e:
         logger.error(f"Erreur lors du démarrage du bot Discord: {e}")
         logger.error(f"Erreur lors du démarrage du bot Discord: {e}")
     finally:
     finally:
         db_manager.close_connection()
         db_manager.close_connection()
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
-    main()
+    main()

+ 1 - 0
requirements.txt

@@ -1,4 +1,5 @@
 mysql-connector-python==9.0.0
 mysql-connector-python==9.0.0
+pytz==2024.2
 pillow==10.4.0
 pillow==10.4.0
 tiktoken==0.7.0
 tiktoken==0.7.0
 discord.py==2.4.0
 discord.py==2.4.0