chatbot.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import discord
  2. from discord.ext import commands
  3. import requests
  4. import json
  5. import os
  6. import random
  7. from dotenv import load_dotenv
  8. from datetime import datetime
  9. import logging
  10. from logging.handlers import RotatingFileHandler
  11. # Configuration du logger
  12. logger = logging.getLogger('discord_bot')
  13. logger.setLevel(logging.INFO)
  14. formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
  15. # Créer un gestionnaire de fichier avec rotation
  16. file_handler = RotatingFileHandler('bot.log', maxBytes=5*1024*1024, backupCount=2) # 5 Mo par fichier, garder 2 sauvegardes
  17. file_handler.setFormatter(formatter)
  18. logger.addHandler(file_handler)
  19. # Optionnel : ajouter un gestionnaire de console pour afficher les logs dans la console
  20. console_handler = logging.StreamHandler()
  21. console_handler.setFormatter(formatter)
  22. logger.addHandler(console_handler)
  23. # Charger les variables d'environnement
  24. load_dotenv()
  25. # Version du bot
  26. VERSION = "4.1.1" # Modifiable selon la version actuelle
  27. # Récupérer les variables d'environnement
  28. MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY')
  29. DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
  30. CHANNEL_ID = int(os.getenv('CHANNEL_ID'))
  31. # Endpoint API Mistral
  32. MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions"
  33. # Fichier pour stocker l'historique
  34. HISTORY_FILE = "conversation_history.json"
  35. MAX_HISTORY_LENGTH = 10 # Limité à 10 messages
  36. def load_history():
  37. """Charge l'historique depuis un fichier JSON."""
  38. if os.path.exists(HISTORY_FILE):
  39. with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
  40. try:
  41. data = json.load(f)
  42. # Vérifier et limiter la taille de chaque historique
  43. for channel_id in data:
  44. if "messages" in data[channel_id]:
  45. if len(data[channel_id]["messages"]) > MAX_HISTORY_LENGTH:
  46. data[channel_id]["messages"] = data[channel_id]["messages"][-MAX_HISTORY_LENGTH:]
  47. return data
  48. except json.JSONDecodeError:
  49. logger.error("Erreur de lecture du fichier d'historique. Création d'un nouveau fichier.")
  50. return {}
  51. return {}
  52. def save_history(history):
  53. """Sauvegarde l'historique dans un fichier JSON."""
  54. with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
  55. json.dump(history, f, ensure_ascii=False, indent=4)
  56. def get_personality_prompt():
  57. try:
  58. with open('personality_prompt.txt', 'r', encoding='utf-8') as file:
  59. return file.read().strip()
  60. except FileNotFoundError:
  61. logger.error("Le fichier personality_prompt.txt n'a pas été trouvé. Utilisation d'un prompt par défaut.")
  62. return """Tu es un assistant utile et poli qui peut analyser des images.
  63. Quand on te montre une image, décris-la et donne ton avis si on te le demande.
  64. Réponds toujours en français avec un ton naturel et amical.
  65. Lorsque tu analyses une image, décris d'abord ce que tu vois en détail,
  66. puis réponds à la question si elle est posée. Utilise un langage clair et accessible."""
  67. # Charger l'historique au démarrage
  68. conversation_history = load_history()
  69. intents = discord.Intents.default()
  70. intents.messages = True
  71. intents.message_content = True
  72. intents.presences = True
  73. bot = commands.Bot(command_prefix='!', intents=intents)
  74. @bot.event
  75. async def on_ready():
  76. logger.info(f'Le bot est connecté en tant que {bot.user}')
  77. global conversation_history
  78. conversation_history = load_history()
  79. # Récupérer le canal spécifié
  80. channel = bot.get_channel(CHANNEL_ID)
  81. if channel is not None:
  82. # Trouver la guilde (serveur) à laquelle appartient le canal
  83. guild = channel.guild
  84. if guild is not None:
  85. # Récupérer le membre du bot sur cette guilde
  86. bot_member = guild.me
  87. # Récupérer le pseudo du bot sur cette guilde
  88. bot_nickname = bot_member.display_name
  89. else:
  90. bot_nickname = bot.user.name # Utiliser le nom global si la guilde n'est pas trouvée
  91. # Créer un embed avec le pseudo du bot
  92. embed = discord.Embed(
  93. title="Bot en ligne",
  94. description=f"{bot_nickname} est désormais en ligne. Version {VERSION}.",
  95. color=discord.Color.green()
  96. )
  97. # Envoyer l'embed dans le canal
  98. await channel.send(embed=embed)
  99. def call_mistral_api(prompt, history, image_url=None, user_id=None, username=None):
  100. headers = {
  101. "Content-Type": "application/json",
  102. "Authorization": f"Bearer {MISTRAL_API_KEY}"
  103. }
  104. personality_prompt = get_personality_prompt()
  105. # Vérifier si la structure messages existe
  106. if "messages" not in history:
  107. history["messages"] = []
  108. # Création du message utilisateur selon qu'il y a une image ou non
  109. if image_url:
  110. # Format multimodal pour les messages avec image
  111. user_content = [
  112. {"type": "text", "text": f"{username}: {prompt}" if username else prompt},
  113. {
  114. "type": "image_url",
  115. "image_url": {
  116. "url": image_url,
  117. "detail": "high" # Demander une analyse détaillée de l'image
  118. }
  119. }
  120. ]
  121. user_message = {
  122. "role": "user",
  123. "content": user_content
  124. }
  125. else:
  126. # Format standard pour les messages texte seulement
  127. user_content = f"{username}: {prompt}" if username else prompt
  128. user_message = {"role": "user", "content": user_content}
  129. # Ajouter le message utilisateur à l'historique
  130. history["messages"].append(user_message)
  131. # Limiter l'historique à MAX_HISTORY_LENGTH messages
  132. if len(history["messages"]) > MAX_HISTORY_LENGTH:
  133. history["messages"] = history["messages"][-MAX_HISTORY_LENGTH:]
  134. # Préparer les messages pour l'API
  135. messages = []
  136. # Ajouter le message système en premier
  137. messages.append({"role": "system", "content": personality_prompt})
  138. # Ajouter l'historique des messages (en gardant le format)
  139. for msg in history["messages"]:
  140. if isinstance(msg["content"], list): # C'est un message multimodal
  141. messages.append({
  142. "role": msg["role"],
  143. "content": msg["content"]
  144. })
  145. else: # C'est un message texte standard
  146. messages.append({
  147. "role": msg["role"],
  148. "content": msg["content"]
  149. })
  150. data = {
  151. "model": "pixtral-large-latest",
  152. "messages": messages,
  153. "max_tokens": 1000
  154. }
  155. try:
  156. response = requests.post(MISTRAL_API_URL, headers=headers, data=json.dumps(data))
  157. response.raise_for_status() # Lève une exception pour les erreurs HTTP
  158. if response.status_code == 200:
  159. response_data = response.json()
  160. # Vérifier si la réponse contient bien le champ attendu
  161. if 'choices' in response_data and len(response_data['choices']) > 0:
  162. assistant_response = response_data['choices'][0]['message']['content']
  163. # Ajouter la réponse de l'assistant à l'historique
  164. history["messages"].append({"role": "assistant", "content": assistant_response})
  165. # Limiter à nouveau après avoir ajouté la réponse
  166. if len(history["messages"]) > MAX_HISTORY_LENGTH:
  167. history["messages"] = history["messages"][-MAX_HISTORY_LENGTH:]
  168. # Sauvegarder l'historique après chaque modification
  169. save_history(conversation_history)
  170. return assistant_response
  171. else:
  172. logger.error(f"Réponse API inattendue: {response_data}")
  173. return "Désolé, je n'ai pas reçu de réponse valide de l'API."
  174. else:
  175. return f"Erreur API: {response.status_code}"
  176. except requests.exceptions.RequestException as e:
  177. logger.error(f"Erreur lors de l'appel API: {e}")
  178. return "Désolé, une erreur réseau est survenue lors de la communication avec l'API."
  179. @bot.command(name='reset')
  180. async def reset_history(ctx):
  181. channel_id = str(ctx.channel.id)
  182. if channel_id in conversation_history:
  183. # Conserver le même ID de conversation mais vider les messages
  184. if "messages" in conversation_history[channel_id]:
  185. conversation_history[channel_id]["messages"] = []
  186. else:
  187. conversation_history[channel_id] = {
  188. "conversation_id": conversation_history[channel_id].get("conversation_id", str(len(conversation_history) + 1)),
  189. "messages": []
  190. }
  191. save_history(conversation_history)
  192. await ctx.send("L'historique de conversation a été réinitialisé.")
  193. else:
  194. conversation_id = str(len(conversation_history) + 1)
  195. conversation_history[channel_id] = {
  196. "conversation_id": conversation_id,
  197. "messages": []
  198. }
  199. save_history(conversation_history)
  200. await ctx.send("Aucun historique de conversation trouvé pour ce channel. Créé un nouvel historique.")
  201. @bot.event
  202. async def on_message(message):
  203. # Ignorer les messages du bot lui-même
  204. if message.author == bot.user:
  205. return
  206. # Vérifier si le message contient des stickers
  207. if message.stickers:
  208. # Obtenir le serveur (guild) du message
  209. guild = message.guild
  210. if guild:
  211. # Obtenir la liste des stickers personnalisés du serveur
  212. stickers = guild.stickers
  213. if stickers:
  214. # Mélanger la liste des stickers pour essayer dans un ordre aléatoire
  215. random_stickers = random.sample(stickers, len(stickers))
  216. for sticker in random_stickers:
  217. try:
  218. logger.info(f"Envoi du sticker: {sticker.name} (ID: {sticker.id})")
  219. await message.channel.send(stickers=[sticker])
  220. break # Si ça marche, on sort de la boucle
  221. except discord.errors.Forbidden as e:
  222. logger.error(f"Erreur lors de l'envoi du sticker: {sticker.name} (ID: {sticker.id}). Erreur: {e}")
  223. continue
  224. else:
  225. # Si aucun sticker n'a pu être envoyé
  226. logger.error("Aucun sticker utilisable trouvé sur ce serveur.")
  227. await message.channel.send("Aucun sticker utilisable trouvé sur ce serveur.")
  228. else:
  229. await message.channel.send("Aucun sticker personnalisé trouvé sur ce serveur.")
  230. else:
  231. await message.channel.send("Ce message ne provient pas d'un serveur.")
  232. return # Retourner pour éviter que le reste du code ne s'exécute
  233. # Vérifier si le message provient du channel spécifique
  234. if message.channel.id != CHANNEL_ID:
  235. return
  236. # Si le message commence par le préfixe du bot, traiter comme une commande
  237. if message.content.startswith('!'):
  238. await bot.process_commands(message)
  239. return
  240. # Résolution des mentions dans le message
  241. resolved_content = message.content
  242. for user in message.mentions:
  243. # Remplacer chaque mention par le nom d'utilisateur
  244. resolved_content = resolved_content.replace(f"<@{user.id}>", f"@{user.display_name}")
  245. # Vérifier le nombre d'images dans le message
  246. image_count = 0
  247. if message.attachments:
  248. for attachment in message.attachments:
  249. if attachment.content_type and attachment.content_type.startswith('image/'):
  250. image_count += 1
  251. # Vérifier si le nombre d'images dépasse la limite
  252. if image_count > 3:
  253. await message.channel.send("Erreur : Vous ne pouvez pas envoyer plus de trois images dans un seul message. Veuillez diviser votre envoi en plusieurs messages.")
  254. return
  255. # Vérifier les pièces jointes pour la taille des images
  256. if message.attachments:
  257. # D'abord, vérifier toutes les images pour leur taille
  258. too_large_images = []
  259. for attachment in message.attachments:
  260. if attachment.content_type and attachment.content_type.startswith('image/'):
  261. max_size = 5 * 1024 * 1024 # 2 Mo en octets
  262. if attachment.size > max_size:
  263. too_large_images.append(attachment.filename)
  264. # Si des images trop grandes sont trouvées, envoyer un message d'erreur et arrêter
  265. if too_large_images:
  266. image_list = ", ".join(too_large_images)
  267. await message.channel.send(f"Erreur : Les images suivantes dépassent la limite de 5 Mo : {image_list}. Veuillez envoyer des images plus petites.")
  268. return
  269. # Récupérer ou initialiser l'historique pour ce channel
  270. channel_id = str(message.channel.id)
  271. global conversation_history
  272. # Charger l'historique actuel
  273. conversation_history = load_history()
  274. if channel_id not in conversation_history:
  275. conversation_id = str(len(conversation_history) + 1)
  276. conversation_history[channel_id] = {
  277. "conversation_id": conversation_id,
  278. "messages": []
  279. }
  280. # Assurer que la clé messages existe
  281. if "messages" not in conversation_history[channel_id]:
  282. conversation_history[channel_id]["messages"] = []
  283. # Traitement des images dans le message
  284. image_url = None
  285. if message.attachments:
  286. for attachment in message.attachments:
  287. if attachment.content_type and attachment.content_type.startswith('image/'):
  288. image_url = attachment.url
  289. break
  290. # Utiliser le contenu résolu (avec les mentions remplacées)
  291. prompt = resolved_content
  292. # Indiquer que le bot est en train de taper
  293. async with message.channel.typing():
  294. try:
  295. # Appeler l'API Mistral avec l'historique de conversation, l'image si présente, et les infos de l'utilisateur
  296. response = call_mistral_api(
  297. prompt,
  298. conversation_history[channel_id],
  299. image_url,
  300. user_id=str(message.author.id),
  301. username=message.author.display_name
  302. )
  303. await message.channel.send(response)
  304. except Exception as e:
  305. logger.error(f"Erreur lors de l'appel à l'API: {e}")
  306. await message.channel.send("Désolé, une erreur est survenue lors du traitement de votre demande.")
  307. # Assurer que les autres gestionnaires d'événements reçoivent également le message
  308. await bot.process_commands(message)
  309. bot.run(DISCORD_TOKEN)