chatbot.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import os
  2. import openai
  3. import discord
  4. import aiohttp
  5. import asyncio
  6. import base64
  7. import logging
  8. import re
  9. from dotenv import load_dotenv
  10. # Charger les variables d'environnement depuis le fichier .env
  11. load_dotenv()
  12. DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
  13. OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
  14. DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
  15. # Chemin vers le fichier de prompt de personnalité
  16. PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
  17. # Vérifier que les tokens et le prompt de personnalité sont récupérés
  18. if DISCORD_TOKEN is None or OPENAI_API_KEY is None or DISCORD_CHANNEL_ID is None:
  19. raise ValueError("Les tokens ou l'ID du canal ne sont pas définis dans les variables d'environnement.")
  20. if not os.path.isfile(PERSONALITY_PROMPT_FILE):
  21. raise FileNotFoundError(f"Le fichier de prompt de personnalité '{PERSONALITY_PROMPT_FILE}' est introuvable.")
  22. # Lire le prompt de personnalité depuis le fichier
  23. with open(PERSONALITY_PROMPT_FILE, 'r', encoding='utf-8') as f:
  24. PERSONALITY_PROMPT = f.read().strip()
  25. # Log configuration
  26. log_format='%(asctime)-13s : %(name)-15s : %(levelname)-8s : %(message)s'
  27. logging.basicConfig(handlers=[logging.FileHandler("./chatbot.log", 'a', 'utf-8')], format=log_format, level="INFO")
  28. console = logging.StreamHandler()
  29. console.setLevel(logging.INFO)
  30. console.setFormatter(logging.Formatter(log_format))
  31. logger = logging.getLogger("chatbot")
  32. logger.setLevel("INFO")
  33. logging.getLogger('').addHandler(console)
  34. # Initialiser les intents
  35. intents = discord.Intents.default()
  36. intents.message_content = True # Activer l'intent pour les contenus de message
  37. # Initialiser le client Discord avec les intents modifiés
  38. client_discord = discord.Client(intents=intents)
  39. # Initialiser l'API OpenAI avec un client
  40. client_openai = openai.OpenAI(api_key=OPENAI_API_KEY)
  41. # Liste pour stocker l'historique des conversations
  42. conversation_history = []
  43. # Convertir l'ID du channel en entier
  44. chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
  45. def is_ascii_art(text):
  46. # Définir un seuil pour la longueur d'une séquence de caractères spéciaux
  47. threshold_length = 10
  48. # Chercher des séquences de caractères spéciaux
  49. special_char_sequences = re.findall(r'[^a-zA-Z0-9\s]{' + str(threshold_length) + ',}', text)
  50. # Si on trouve une séquence de caractères spéciaux longue, c'est probablement un dessin ASCII
  51. if any(len(seq) >= threshold_length for seq in special_char_sequences):
  52. return True
  53. return False
  54. def is_long_special_text(text):
  55. # Définir un seuil pour considérer le texte comme long et contenant beaucoup de caractères spéciaux
  56. special_char_count = len(re.findall(r'[^\w\s]', text))
  57. if len(text) > 1200 and special_char_count > 200:
  58. return True
  59. return False
  60. def calculate_cost(usage):
  61. input_tokens = usage.get('prompt_tokens', 0)
  62. output_tokens = usage.get('completion_tokens', 0)
  63. # Coûts estimés
  64. input_cost = input_tokens / 1_000_000 * 5.00 # 5$ pour 1M tokens d'entrée
  65. output_cost = output_tokens / 1_000_000 * 15.00 # 15$ pour 1M tokens de sortie
  66. total_cost = input_cost + output_cost
  67. return input_tokens, output_tokens, total_cost
  68. async def read_text_file(attachment):
  69. # Télécharger et lire le contenu du fichier texte
  70. async with aiohttp.ClientSession() as session:
  71. async with session.get(attachment.url) as resp:
  72. return await resp.text()
  73. async def encode_image_from_attachment(attachment):
  74. async with aiohttp.ClientSession() as session:
  75. async with session.get(attachment.url) as resp:
  76. image_data = await resp.read()
  77. return base64.b64encode(image_data).decode('utf-8')
  78. async def call_openai_api(user_text, user_name, image_data=None):
  79. # Préparer le contenu pour l'appel API
  80. message_to_send = {
  81. "role": "user",
  82. "content": [{"type": "text", "text": f"{user_name} dit : {user_text}"}]
  83. }
  84. # Inclure l'image dans l'appel API courant
  85. if image_data:
  86. message_to_send["content"].append({
  87. "type": "image_url",
  88. "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}
  89. })
  90. if not conversation_history:
  91. conversation_history.append({
  92. "role": "system",
  93. "content": PERSONALITY_PROMPT
  94. })
  95. # Ajouter le message de l'utilisateur à l'historique global, mais uniquement s'il ne s'agit pas d'une image ou d'ASCII art
  96. if image_data is None and not is_ascii_art(user_text):
  97. conversation_history.append(message_to_send)
  98. payload = {
  99. "model": "gpt-4o",
  100. "messages": conversation_history,
  101. "max_tokens": 500
  102. }
  103. headers = {
  104. "Content-Type": "application/json",
  105. "Authorization": f"Bearer {OPENAI_API_KEY}"
  106. }
  107. try:
  108. async with aiohttp.ClientSession() as session:
  109. async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) as resp:
  110. result = await resp.json()
  111. if resp.status != 200:
  112. raise ValueError(f"API Error: {result.get('error', {}).get('message', 'Unknown error')}")
  113. # Calculer les coûts
  114. usage = result.get('usage', {})
  115. input_tokens, output_tokens, total_cost = calculate_cost(usage)
  116. # Afficher dans la console
  117. logging.info(f"Estimated Cost: ${total_cost:.4f} / Input Tokens: {input_tokens} / Output Tokens: {output_tokens} / Total Tokens: {input_tokens + output_tokens}")
  118. return result
  119. except Exception as e:
  120. logger.error(f"Error calling OpenAI API: {e}")
  121. return None
  122. @client_discord.event
  123. async def on_ready():
  124. logger.info(f'Bot connecté en tant que {client_discord.user}')
  125. # Ajouter la personnalité de l'IA à l'historique au démarrage
  126. if not conversation_history:
  127. conversation_history.append({
  128. "role": "system",
  129. "content": PERSONALITY_PROMPT
  130. })
  131. @client_discord.event
  132. async def on_message(message):
  133. # Vérifier si le message provient du canal autorisé
  134. if message.channel.id != chatgpt_channel_id:
  135. return
  136. # Vérifier si l'auteur du message est le bot lui-même
  137. if message.author == client_discord.user:
  138. return
  139. user_text = message.content.strip()
  140. image_data = None
  141. file_content = None
  142. # Extensions de fichiers autorisées
  143. allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
  144. # Vérifier s'il y a une pièce jointe
  145. if message.attachments:
  146. for attachment in message.attachments:
  147. # Vérifier si c'est un fichier avec une extension autorisée
  148. if any(attachment.filename.endswith(ext) for ext in allowed_extensions):
  149. file_content = await read_text_file(attachment)
  150. break
  151. # Vérifier si c'est une image
  152. elif attachment.content_type.startswith('image/'):
  153. image_data = await encode_image_from_attachment(attachment)
  154. break
  155. # Ajouter le contenu du fichier à la requête si présent
  156. if file_content:
  157. user_text += f"\nContenu du fichier {attachment.filename}:\n{file_content}"
  158. # Appeler l'API OpenAI
  159. result = await call_openai_api(user_text, message.author.name, image_data)
  160. if result:
  161. reply = result['choices'][0]['message']['content']
  162. await message.channel.send(reply)
  163. # Ajouter la réponse du modèle à l'historique
  164. # Ne pas ajouter à l'historique si c'est un dessin ASCII ou une image
  165. if image_data is None and not is_ascii_art(user_text):
  166. add_to_conversation_history({
  167. "role": "assistant",
  168. "content": reply
  169. })
  170. MAX_HISTORY_LENGTH = 50 # Nombre maximum de messages à conserver
  171. # Liste pour stocker les indices des messages longs et spéciaux
  172. temporary_messages = []
  173. def add_to_conversation_history(new_message):
  174. # Ajouter la personnalité de l'IA en tant que premier message
  175. if not conversation_history:
  176. conversation_history.append({
  177. "role": "system",
  178. "content": PERSONALITY_PROMPT
  179. })
  180. # Ajouter le message à l'historique
  181. conversation_history.append(new_message)
  182. # Vérifier si le message est long et contient beaucoup de caractères spéciaux
  183. if new_message["role"] == "user" and is_long_special_text(new_message["content"][0]["text"]):
  184. # Ajouter l'index de ce message dans la liste des messages temporaires
  185. temporary_messages.append(len(conversation_history) - 1)
  186. # Limiter la taille de l'historique
  187. if len(conversation_history) > MAX_HISTORY_LENGTH:
  188. # Garder le premier message de personnalité et les messages les plus récents
  189. conversation_history[:] = conversation_history[:1] + conversation_history[-MAX_HISTORY_LENGTH:]
  190. # Supprimer les messages temporaires après dix messages
  191. if len(temporary_messages) > 0:
  192. for index in reversed(temporary_messages):
  193. # Supprimer le message s'il a été dans l'historique pendant dix messages ou plus
  194. if len(conversation_history) - index > 10:
  195. del conversation_history[index]
  196. temporary_messages.remove(index)
  197. # Démarrer le bot Discord
  198. client_discord.run(DISCORD_TOKEN)