1
0

chatbot.py 14 KB

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