chatbot.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import os
  2. import logging
  3. import discord
  4. from dotenv import load_dotenv
  5. from openai import AsyncOpenAI, OpenAIError
  6. import json
  7. import urllib3
  8. # Charger les variables d'environnement depuis le fichier .env
  9. load_dotenv()
  10. DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
  11. OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
  12. DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
  13. PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
  14. CONVERSATION_HISTORY_FILE = os.getenv('CONVERSATION_HISTORY_FILE', 'conversation_history.json')
  15. CONVERSATION_HISTORY_SIZE = int(os.getenv('CONVERSATION_HISTORY_SIZE', '50'))
  16. BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
  17. MODEL = os.getenv('MODEL', 'gpt-4')
  18. URL_OPENAI_API = os.getenv('URL_OPENAI_API', 'http://localai.localai.svc.cluster.local:8080/v1')
  19. TEMPERATURE = float(os.getenv('TEMPERATURE', "1.0"))
  20. # Initialiser le client OpenAI asynchrone ici
  21. openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY, base_url=URL_OPENAI_API)
  22. BOT_VERSION = "2.5.3-penta"
  23. # Vérifier que les tokens et le prompt de personnalité sont récupérés
  24. if DISCORD_TOKEN is None or OPENAI_API_KEY is None or DISCORD_CHANNEL_ID is None:
  25. raise ValueError("Les tokens ou l'ID du canal ne sont pas définis dans les variables d'environnement.")
  26. if not os.path.isfile(PERSONALITY_PROMPT_FILE):
  27. raise FileNotFoundError(f"Le fichier de prompt de personnalité '{PERSONALITY_PROMPT_FILE}' est introuvable.")
  28. # Lire le prompt de personnalité depuis le fichier
  29. with open(PERSONALITY_PROMPT_FILE, 'r', encoding='utf-8') as f:
  30. PERSONALITY_PROMPT = f.read().strip()
  31. # Log configuration
  32. log_format = '%(asctime)-13s : %(name)-15s : %(levelname)-8s : %(message)s'
  33. logging.basicConfig(handlers=[logging.FileHandler("./chatbot.log", 'a', 'utf-8')], format=log_format, level="INFO")
  34. console = logging.StreamHandler()
  35. console.setLevel(logging.INFO)
  36. console.setFormatter(logging.Formatter(log_format))
  37. logger = logging.getLogger(BOT_NAME)
  38. logger.setLevel("INFO")
  39. logging.getLogger('').addHandler(console)
  40. httpx_logger = logging.getLogger('httpx')
  41. httpx_logger.setLevel(logging.WARNING)
  42. urllib3.disable_warnings()
  43. # Initialiser les intents
  44. intents = discord.Intents.default()
  45. intents.message_content = True # Activer l'intent pour les contenus de message
  46. # Liste pour stocker l'historique des conversations
  47. conversation_history = []
  48. def filter_message(message):
  49. """Filtre le contenu d'un retour de modèle de language, comme pour enlever les pensées dans le cas de DeepSeek"""
  50. if len(message.split('</think>')) > 1:
  51. result = message.split('</think>')[1]
  52. elif len(message.split('</response>')) > 1:
  53. result = message.split('</response>')[1]
  54. return result
  55. def transorm_emote(message, output : bool):
  56. """Remplace les smileys par les codes Discord correspondant"""
  57. result = message
  58. return result
  59. def split_message(message, max_length=2000):
  60. """Divise un message en plusieurs segments de longueur maximale spécifiée."""
  61. if len(message) <= max_length:
  62. return [message]
  63. parts = []
  64. current_part = ""
  65. for line in message.split('\n'):
  66. if len(current_part) + len(line) + 1 > max_length:
  67. parts.append(current_part)
  68. current_part = line + '\n'
  69. else:
  70. current_part += line + '\n'
  71. if current_part:
  72. parts.append(current_part)
  73. return parts
  74. def load_conversation_history():
  75. global conversation_history
  76. if os.path.isfile(CONVERSATION_HISTORY_FILE):
  77. try:
  78. with open(CONVERSATION_HISTORY_FILE, 'r', encoding='utf-8') as f:
  79. loaded_history = json.load(f)
  80. # Exclure uniquement le PERSONALITY_PROMPT
  81. conversation_history = [
  82. msg for msg in loaded_history
  83. if not (msg.get("role") == "system" and msg.get("content") == PERSONALITY_PROMPT)
  84. ]
  85. logger.info(f"Historique chargé depuis {CONVERSATION_HISTORY_FILE}")
  86. except Exception as e:
  87. logger.error(f"Erreur lors du chargement de l'historique : {e}")
  88. conversation_history = []
  89. else:
  90. logger.info(f"Aucun fichier d'historique trouvé. Un nouveau fichier sera créé à {CONVERSATION_HISTORY_FILE}")
  91. def has_text(text):
  92. """
  93. Détermine si le texte fourni est non vide après suppression des espaces.
  94. """
  95. return bool(text.strip())
  96. # Fonction de sauvegarde de l'historique
  97. def save_conversation_history():
  98. try:
  99. with open(CONVERSATION_HISTORY_FILE, 'w', encoding='utf-8') as f:
  100. json.dump(conversation_history, f, ensure_ascii=False, indent=4)
  101. except Exception as e:
  102. logger.error(f"Erreur lors de la sauvegarde de l'historique : {e}")
  103. # Convertir l'ID du channel en entier
  104. try:
  105. chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
  106. except ValueError:
  107. raise ValueError("L'ID du channel Discord est invalide. Assurez-vous qu'il s'agit d'un entier.")
  108. class MyDiscordClient(discord.Client):
  109. async def close(self):
  110. global openai_client
  111. if openai_client is not None:
  112. await openai_client.close()
  113. openai_client = None
  114. await super().close()
  115. # Initialiser le client Discord avec les intents modifiés
  116. client_discord = MyDiscordClient(intents=intents)
  117. # Appeler la fonction pour charger l'historique au démarrage
  118. load_conversation_history()
  119. def extract_text_from_message(message):
  120. content = message.get("content", "")
  121. if isinstance(content, list):
  122. # Extraire le texte de chaque élément de la liste
  123. texts = []
  124. for part in content:
  125. if isinstance(part, dict):
  126. text = part.get("text", "")
  127. if text:
  128. texts.append(text)
  129. return ' '.join(texts)
  130. elif isinstance(content, str):
  131. return content
  132. else:
  133. return ""
  134. async def read_text_file(attachment):
  135. file_bytes = await attachment.read()
  136. return file_bytes.decode('utf-8')
  137. async def call_openai_api(user_text, user_name, detail='high'):
  138. # Préparer le contenu pour l'appel API
  139. message_to_send = {
  140. "role": "user",
  141. "content": [
  142. {"type": "text", "text": f"{user_name} dit : {transorm_emote(user_text, False)}"}
  143. ]
  144. }
  145. # Assembler les messages avec le prompt de personnalité en premier
  146. messages = [
  147. {"role": "system", "content": PERSONALITY_PROMPT}
  148. ] + conversation_history + [message_to_send]
  149. try:
  150. response = await openai_client.chat.completions.create(
  151. model=MODEL,
  152. messages=messages,
  153. temperature=TEMPERATURE
  154. )
  155. if response:
  156. reply = response.choices[0].message.content
  157. await add_to_conversation_history(message_to_send)
  158. # Ajouter la réponse de l'IA directement à l'historique
  159. await add_to_conversation_history({
  160. "role": "assistant",
  161. "content": reply
  162. })
  163. return response
  164. except Exception as e:
  165. logger.error(f"Erreur durant l'appel de l'API : {e}")
  166. return None
  167. @client_discord.event
  168. async def on_ready():
  169. logger.info(f'{BOT_NAME} connecté en tant que {client_discord.user}')
  170. logger.info(f'Utilisation du modèle {MODEL}')
  171. if not conversation_history:
  172. logger.info("Aucun historique trouvé. L'historique commence vide.")
  173. # Envoyer un message de version dans le canal Discord
  174. channel = client_discord.get_channel(chatgpt_channel_id)
  175. if channel:
  176. try:
  177. embed = discord.Embed(
  178. title=f"Bot Démarré",
  179. description=f"🎉 {BOT_NAME} est en ligne ! Version {BOT_VERSION}\nUtilisation du modèle: **{MODEL}**",
  180. color=0x2222aa # Bleu
  181. )
  182. await channel.send(embed=embed)
  183. logger.info(f"Message de connexion envoyé dans le canal ID {chatgpt_channel_id}")
  184. except discord.Forbidden:
  185. logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal ID {chatgpt_channel_id}.")
  186. except discord.HTTPException as e:
  187. logger.error(f"Erreur lors de l'envoi du message de connexion : {e}")
  188. else:
  189. logger.error(f"Canal avec ID {chatgpt_channel_id} non trouvé.")
  190. @client_discord.event
  191. async def on_message(message):
  192. global conversation_history
  193. # Vérifier si le message provient du canal autorisé
  194. if message.channel.id != chatgpt_channel_id:
  195. return
  196. # Ignorer les messages du bot lui-même
  197. if message.author == client_discord.user:
  198. return
  199. user_text = message.content.strip()
  200. file_content = None
  201. attachment_filename = None
  202. # Vérifier si le message est la commande de réinitialisation
  203. if user_text.lower() == "!reset_history":
  204. # Vérifier si l'utilisateur a les permissions administratives
  205. if not message.author.guild_permissions.administrator:
  206. await message.channel.send("❌ Vous n'avez pas la permission d'utiliser cette commande.")
  207. return
  208. conversation_history = []
  209. save_conversation_history()
  210. await message.channel.send("✅ L'historique des conversations a été réinitialisé.")
  211. logger.info(f"Historique des conversations réinitialisé par {message.author}.")
  212. return # Arrêter le traitement du message après la réinitialisation
  213. # Extensions de fichiers autorisées
  214. allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
  215. # Variable pour stocker si le message contient un fichier
  216. has_file = False
  217. # Vérifier s'il y a une pièce jointe
  218. if message.attachments:
  219. for attachment in message.attachments:
  220. # Vérifier si c'est un fichier avec une extension autorisée
  221. if any(attachment.filename.endswith(ext) for ext in allowed_extensions):
  222. file_content = await read_text_file(attachment)
  223. attachment_filename = attachment.filename
  224. break
  225. # Ajouter le contenu du fichier à la requête si présent
  226. if file_content:
  227. user_text += f"\nContenu du fichier {attachment.filename}:\n{file_content}"
  228. # Vérifier si le texte n'est pas vide après ajout du contenu du fichier
  229. if not has_text(user_text):
  230. return # Ne pas appeler l'API si le texte est vide
  231. async with message.channel.typing():
  232. try:
  233. # Appeler l'API OpenAI
  234. result = await call_openai_api(user_text, message.author.name)
  235. if result:
  236. reply = result.choices[0].message.content
  237. reply = reply.rstrip("</s>")
  238. reply = filter_message(reply)
  239. reply = transorm_emote(reply, True)
  240. message_parts = split_message(reply)
  241. for part in message_parts:
  242. await message.channel.send(part)
  243. # Afficher dans la console
  244. logging.info(f"Réponse envoyée. ({len(message_parts)} message(s))")
  245. except Exception as e:
  246. await message.channel.send("Franchement, je sais pas quoi te répondre. <:haptriste:358054014262181889>")
  247. logger.error(f"Erreur lors du traitement du texte: {e}")
  248. async def add_to_conversation_history(new_message):
  249. global conversation_history
  250. # Ne pas ajouter le PERSONALITY_PROMPT à l'historique
  251. if new_message.get("role") == "system" and new_message.get("content") == PERSONALITY_PROMPT:
  252. logger.debug("PERSONALITY_PROMPT système non ajouté à l'historique.")
  253. return
  254. conversation_history.append(new_message)
  255. save_conversation_history()
  256. logger.debug(f"Message ajouté à l'historique. Taille actuelle : {len(conversation_history)}")
  257. if len(conversation_history) > CONVERSATION_HISTORY_SIZE:
  258. logger.info(f"Limite de {CONVERSATION_HISTORY_SIZE} messages atteinte.")
  259. excess_messages = len(conversation_history) - CONVERSATION_HISTORY_SIZE
  260. if excess_messages > 0:
  261. # Supprimer les messages les plus anciens
  262. del conversation_history[:excess_messages]
  263. save_conversation_history()
  264. logger.info(f"{excess_messages} messages les plus anciens ont été supprimés pour maintenir l'historique à {CONVERSATION_HISTORY_SIZE} messages.")
  265. # Démarrer le bot Discord
  266. client_discord.run(DISCORD_TOKEN)