1
0

chatbot.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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. # Compter les caractères spéciaux et le nombre total de caractères
  47. special_char_count = len(re.findall(r'[^\w\s]', text))
  48. total_chars = len(text)
  49. # Définir des critères pour détecter des dessins ASCII
  50. density_threshold = 0.2 # Proportion minimale de caractères spéciaux
  51. min_lines = 3 # Nombre minimum de lignes pour considérer comme un dessin ASCII
  52. # Vérifier la densité des caractères spéciaux
  53. if total_chars > 0 and (special_char_count / total_chars) > density_threshold:
  54. # Vérifier la structure des lignes
  55. lines = text.split('\n')
  56. if len(lines) >= min_lines:
  57. average_length = sum(len(line) for line in lines) / len(lines)
  58. similar_length_lines = sum(1 for line in lines if abs(len(line) - average_length) < 5)
  59. # Si la plupart des lignes ont une longueur similaire, c'est probablement un dessin ASCII
  60. if similar_length_lines >= len(lines) * 0.8:
  61. return True
  62. return False
  63. def is_long_special_text(text):
  64. # Définir un seuil pour considérer le texte comme long et contenant beaucoup de caractères spéciaux
  65. special_char_count = len(re.findall(r'[^\w\s]', text))
  66. if len(text) > 800 or special_char_count > 50:
  67. return True
  68. return False
  69. def calculate_cost(usage):
  70. input_tokens = usage.get('prompt_tokens', 0)
  71. output_tokens = usage.get('completion_tokens', 0)
  72. # Coûts estimés
  73. input_cost = input_tokens / 1_000_000 * 5.00 # 5$ pour 1M tokens d'entrée
  74. output_cost = output_tokens / 1_000_000 * 15.00 # 15$ pour 1M tokens de sortie
  75. total_cost = input_cost + output_cost
  76. return input_tokens, output_tokens, total_cost
  77. async def read_text_file(attachment):
  78. # Télécharger et lire le contenu du fichier texte
  79. async with aiohttp.ClientSession() as session:
  80. async with session.get(attachment.url) as resp:
  81. return await resp.text()
  82. async def encode_image_from_attachment(attachment):
  83. async with aiohttp.ClientSession() as session:
  84. async with session.get(attachment.url) as resp:
  85. image_data = await resp.read()
  86. return base64.b64encode(image_data).decode('utf-8')
  87. async def call_openai_api(user_text, user_name, image_data=None):
  88. # Préparer le contenu pour l'appel API
  89. message_to_send = {
  90. "role": "user",
  91. "content": [{"type": "text", "text": f"{user_name} dit : {user_text}"}]
  92. }
  93. # Inclure l'image dans l'appel API courant
  94. if image_data:
  95. message_to_send["content"].append({
  96. "type": "image_url",
  97. "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}
  98. })
  99. if not conversation_history:
  100. conversation_history.append({
  101. "role": "system",
  102. "content": PERSONALITY_PROMPT
  103. })
  104. # Ajouter le message de l'utilisateur à l'historique global, mais uniquement s'il ne s'agit pas d'une image ou d'ASCII art
  105. if image_data is None and not is_ascii_art(user_text):
  106. conversation_history.append(message_to_send)
  107. payload = {
  108. "model": "gpt-4o",
  109. "messages": conversation_history,
  110. "max_tokens": 500
  111. }
  112. headers = {
  113. "Content-Type": "application/json",
  114. "Authorization": f"Bearer {OPENAI_API_KEY}"
  115. }
  116. try:
  117. async with aiohttp.ClientSession() as session:
  118. async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) as resp:
  119. result = await resp.json()
  120. if resp.status != 200:
  121. raise ValueError(f"API Error: {result.get('error', {}).get('message', 'Unknown error')}")
  122. # Calculer les coûts
  123. usage = result.get('usage', {})
  124. input_tokens, output_tokens, total_cost = calculate_cost(usage)
  125. # Afficher dans la console
  126. logging.info(f"Estimated Cost: ${total_cost:.4f} / Input Tokens: {input_tokens} / Output Tokens: {output_tokens} / Total Tokens: {input_tokens + output_tokens}")
  127. return result
  128. except Exception as e:
  129. logger.error(f"Error calling OpenAI API: {e}")
  130. return None
  131. @client_discord.event
  132. async def on_ready():
  133. logger.info(f'Bot connecté en tant que {client_discord.user}')
  134. # Ajouter la personnalité de l'IA à l'historique au démarrage
  135. if not conversation_history:
  136. conversation_history.append({
  137. "role": "system",
  138. "content": PERSONALITY_PROMPT
  139. })
  140. @client_discord.event
  141. async def on_message(message):
  142. # Vérifier si le message provient du canal autorisé
  143. if message.channel.id != chatgpt_channel_id:
  144. return
  145. # Vérifier si l'auteur du message est le bot lui-même
  146. if message.author == client_discord.user:
  147. return
  148. user_text = message.content.strip()
  149. image_data = None
  150. file_content = None
  151. # Extensions de fichiers autorisées
  152. allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
  153. # Vérifier s'il y a une pièce jointe
  154. if message.attachments:
  155. for attachment in message.attachments:
  156. # Vérifier si c'est un fichier avec une extension autorisée
  157. if any(attachment.filename.endswith(ext) for ext in allowed_extensions):
  158. file_content = await read_text_file(attachment)
  159. break
  160. # Vérifier si c'est une image
  161. elif attachment.content_type.startswith('image/'):
  162. image_data = await encode_image_from_attachment(attachment)
  163. break
  164. # Ajouter le contenu du fichier à la requête si présent
  165. if file_content:
  166. user_text += f"\nContenu du fichier {attachment.filename}:\n{file_content}"
  167. # Appeler l'API OpenAI
  168. result = await call_openai_api(user_text, message.author.name, image_data)
  169. if result:
  170. reply = result['choices'][0]['message']['content']
  171. await message.channel.send(reply)
  172. # Ajouter la réponse du modèle à l'historique
  173. # Ne pas ajouter à l'historique si c'est un dessin ASCII ou une image
  174. if image_data is None and not is_ascii_art(user_text):
  175. add_to_conversation_history({
  176. "role": "assistant",
  177. "content": reply
  178. })
  179. MAX_HISTORY_LENGTH = 50 # Nombre maximum de messages à conserver
  180. # Liste pour stocker les indices des messages longs et spéciaux
  181. temporary_messages = []
  182. def add_to_conversation_history(new_message):
  183. # Ajouter la personnalité de l'IA en tant que premier message
  184. if not conversation_history:
  185. conversation_history.append({
  186. "role": "system",
  187. "content": PERSONALITY_PROMPT
  188. })
  189. # Ajouter le message à l'historique
  190. conversation_history.append(new_message)
  191. # Vérifier si le message est long et contient beaucoup de caractères spéciaux
  192. if new_message["role"] == "user" and is_long_special_text(new_message["content"][0]["text"]):
  193. # Ajouter l'index de ce message dans la liste des messages temporaires
  194. temporary_messages.append(len(conversation_history) - 1)
  195. # Limiter la taille de l'historique
  196. if len(conversation_history) > MAX_HISTORY_LENGTH:
  197. # Garder le premier message de personnalité et les messages les plus récents
  198. conversation_history[:] = conversation_history[:1] + conversation_history[-MAX_HISTORY_LENGTH:]
  199. # Supprimer les messages temporaires après dix messages
  200. if len(temporary_messages) > 0:
  201. for index in reversed(temporary_messages):
  202. # Supprimer le message s'il a été dans l'historique pendant dix messages ou plus
  203. if len(conversation_history) - index > 10:
  204. del conversation_history[index]
  205. temporary_messages.remove(index)
  206. # Démarrer le bot Discord
  207. client_discord.run(DISCORD_TOKEN)