chatbot.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. import os
  2. import base64
  3. import logging
  4. import re
  5. from io import BytesIO
  6. import discord
  7. from dotenv import load_dotenv
  8. from PIL import Image
  9. import emoji
  10. import tiktoken
  11. from openai import AsyncOpenAI, OpenAIError
  12. import json
  13. import urllib3
  14. # Charger les variables d'environnement depuis le fichier .env
  15. load_dotenv()
  16. DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
  17. OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
  18. DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
  19. PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
  20. CONVERSATION_HISTORY_FILE = os.getenv('CONVERSATION_HISTORY_FILE', 'conversation_history.json')
  21. BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
  22. MODEL = os.getenv('MODEL', 'gpt-4o-mini')
  23. URL_OPENAI_API = os.getenv('URL_OPENAI_API', 'http://localai.localai.svc.cluster.local:8080/v1')
  24. TEMPERATURE = float(os.getenv('TEMPERATURE', "1.1"))
  25. # Initialiser le client OpenAI asynchrone ici
  26. openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY, base_url=URL_OPENAI_API)
  27. BOT_VERSION = "2.5.0-penta"
  28. # Vérifier que les tokens et le prompt de personnalité sont récupérés
  29. if DISCORD_TOKEN is None or OPENAI_API_KEY is None or DISCORD_CHANNEL_ID is None:
  30. raise ValueError("Les tokens ou l'ID du canal ne sont pas définis dans les variables d'environnement.")
  31. if not os.path.isfile(PERSONALITY_PROMPT_FILE):
  32. raise FileNotFoundError(f"Le fichier de prompt de personnalité '{PERSONALITY_PROMPT_FILE}' est introuvable.")
  33. # Lire le prompt de personnalité depuis le fichier
  34. with open(PERSONALITY_PROMPT_FILE, 'r', encoding='utf-8') as f:
  35. PERSONALITY_PROMPT = f.read().strip()
  36. # Log configuration
  37. log_format = '%(asctime)-13s : %(name)-15s : %(levelname)-8s : %(message)s'
  38. logging.basicConfig(handlers=[logging.FileHandler("./chatbot.log", 'a', 'utf-8')], format=log_format, level="INFO")
  39. console = logging.StreamHandler()
  40. console.setLevel(logging.INFO)
  41. console.setFormatter(logging.Formatter(log_format))
  42. logger = logging.getLogger(BOT_NAME)
  43. logger.setLevel("INFO")
  44. logging.getLogger('').addHandler(console)
  45. httpx_logger = logging.getLogger('httpx')
  46. httpx_logger.setLevel(logging.WARNING)
  47. urllib3.disable_warnings()
  48. # Initialiser les intents
  49. intents = discord.Intents.default()
  50. intents.message_content = True # Activer l'intent pour les contenus de message
  51. # Liste pour stocker l'historique des conversations
  52. conversation_history = []
  53. # Variable globale pour suivre la position de la dernière analyse
  54. last_analysis_index = None
  55. messages_since_last_analysis = 0
  56. def load_conversation_history():
  57. global conversation_history
  58. if os.path.isfile(CONVERSATION_HISTORY_FILE):
  59. try:
  60. with open(CONVERSATION_HISTORY_FILE, 'r', encoding='utf-8') as f:
  61. loaded_history = json.load(f)
  62. # Exclure uniquement le PERSONALITY_PROMPT
  63. conversation_history = [
  64. msg for msg in loaded_history
  65. if not (msg.get("role") == "system" and msg.get("content") == PERSONALITY_PROMPT)
  66. ]
  67. logger.info(f"Historique chargé depuis {CONVERSATION_HISTORY_FILE}")
  68. except Exception as e:
  69. logger.error(f"Erreur lors du chargement de l'historique : {e}")
  70. conversation_history = []
  71. else:
  72. logger.info(f"Aucun fichier d'historique trouvé. Un nouveau fichier sera créé à {CONVERSATION_HISTORY_FILE}")
  73. def has_text(text):
  74. """
  75. Détermine si le texte fourni est non vide après suppression des espaces.
  76. """
  77. return bool(text.strip())
  78. # Fonction de sauvegarde de l'historique
  79. def save_conversation_history():
  80. try:
  81. with open(CONVERSATION_HISTORY_FILE, 'w', encoding='utf-8') as f:
  82. json.dump(conversation_history, f, ensure_ascii=False, indent=4)
  83. except Exception as e:
  84. logger.error(f"Erreur lors de la sauvegarde de l'historique : {e}")
  85. # Charger l'encodeur pour le modèle GPT-4o mini
  86. encoding = tiktoken.get_encoding("o200k_base")
  87. # Convertir l'ID du channel en entier
  88. try:
  89. chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
  90. except ValueError:
  91. raise ValueError("L'ID du channel Discord est invalide. Assurez-vous qu'il s'agit d'un entier.")
  92. class MyDiscordClient(discord.Client):
  93. async def close(self):
  94. global openai_client
  95. if openai_client is not None:
  96. await openai_client.close()
  97. openai_client = None
  98. await super().close()
  99. # Initialiser le client Discord avec les intents modifiés
  100. client_discord = MyDiscordClient(intents=intents)
  101. # Appeler la fonction pour charger l'historique au démarrage
  102. load_conversation_history()
  103. def resize_image(image_bytes, mode='high', attachment_filename=None):
  104. try:
  105. with Image.open(BytesIO(image_bytes)) as img:
  106. original_format = img.format # Store the original format
  107. if mode == 'high':
  108. # Redimensionner pour le mode haute fidélité
  109. img.thumbnail((2000, 2000))
  110. if min(img.size) < 768:
  111. scale = 768 / min(img.size)
  112. new_size = tuple(int(x * scale) for x in img.size)
  113. img = img.resize(new_size, Image.Resampling.LANCZOS)
  114. elif mode == 'low':
  115. # Redimensionner pour le mode basse fidélité
  116. img = img.resize((512, 512))
  117. buffer = BytesIO()
  118. img_format = img.format
  119. if not img_format:
  120. if attachment_filename:
  121. _, ext = os.path.splitext(attachment_filename)
  122. ext = ext.lower()
  123. format_mapping = {
  124. '.jpg': 'JPEG',
  125. '.jpeg': 'JPEG',
  126. '.png': 'PNG',
  127. '.gif': 'GIF',
  128. '.bmp': 'BMP',
  129. '.tiff': 'TIFF'
  130. }
  131. img_format = format_mapping.get(ext, 'PNG')
  132. else:
  133. img_format = 'PNG'
  134. img.save(buffer, format=img_format)
  135. return buffer.getvalue()
  136. except Exception as e:
  137. logger.error(f"Error resizing image: {e}")
  138. raise
  139. def extract_text_from_message(message):
  140. content = message.get("content", "")
  141. if isinstance(content, list):
  142. # Extraire le texte de chaque élément de la liste
  143. texts = []
  144. for part in content:
  145. if isinstance(part, dict):
  146. text = part.get("text", "")
  147. if text:
  148. texts.append(text)
  149. return ' '.join(texts)
  150. elif isinstance(content, str):
  151. return content
  152. else:
  153. return ""
  154. def calculate_cost(usage, model=MODEL):
  155. input_tokens = usage.get('prompt_tokens', 0)
  156. output_tokens = usage.get('completion_tokens', 0)
  157. # Définir les tarifs par modèle
  158. model_costs = {
  159. 'gpt-4o': {
  160. 'input_rate': 5.00 / 1_000_000, # 5$ pour 1M tokens d'entrée
  161. 'output_rate': 15.00 / 1_000_000 # 15$ pour 1M tokens de sortie
  162. },
  163. 'gpt-4o-mini': {
  164. 'input_rate': 30.00 / 1_000_000, # 30$ pour 1M tokens d'entrée
  165. 'output_rate': 30.00 / 1_000_000 # 30$ pour 1M tokens de sortie
  166. },
  167. 'gpt-4': {
  168. 'input_rate': 0.150 / 1_000_000, # 0.150$ pour 1M tokens d'entrée
  169. 'output_rate': 0.600 / 1_000_000 # 0.600$ pour 1M tokens de sortie
  170. }
  171. }
  172. # Obtenir les tarifs du modèle spécifié
  173. if model not in model_costs:
  174. model = 'gpt-4o'
  175. input_rate = model_costs[model]['input_rate']
  176. output_rate = model_costs[model]['output_rate']
  177. # Calculer les coûts
  178. input_cost = input_tokens * input_rate
  179. output_cost = output_tokens * output_rate
  180. total_cost = input_cost + output_cost
  181. return input_tokens, output_tokens, total_cost
  182. async def call_gpt4o_for_image_analysis(image_data, user_text=None, detail='high'):
  183. try:
  184. # Préparer la requête pour GPT-4o
  185. if user_text:
  186. prompt = (
  187. f"Analyse cette image de manière extrêmement précise en tenant compte de la description suivante : \"{user_text}\"."
  188. "Si des personnages sont présents, décris avec détails leurs vêtements, accessoires et physique."
  189. "Décris leurs courbes, leur taille, leur poids, leurs mensurations."
  190. "Décris comment ils intéragissent avec leur environnement et avec les autres personnages."
  191. )
  192. else:
  193. prompt = (
  194. "Analyse cette image de manière extrêmement précise s'il te plaît."
  195. "Si des personnages sont présents, décris avec détails leurs vêtements, accessoires et physique."
  196. "Décris leurs courbes, leur taille, leur poids, leurs mensurations."
  197. "Décris comment ils intéragissent avec leur environnement et avec les autres personnages."
  198. )
  199. message_to_send = {
  200. "role": "user",
  201. "content": [
  202. {"type": "text", "text": prompt},
  203. {
  204. "type": "image_url",
  205. "image_url": {
  206. "url": f"data:image/jpeg;base64,{image_data}",
  207. "detail": detail
  208. }
  209. }
  210. ]
  211. }
  212. # Appel à GPT-4o
  213. response = await openai_client.chat.completions.create(
  214. model=MODEL,
  215. messages=[message_to_send],
  216. max_tokens=4096
  217. )
  218. if response:
  219. analysis = response.choices[0].message.content
  220. logging.info(f"Analyse de l'image par {str.capitalize(MODEL)} : {analysis}")
  221. # Calcul et affichage du coût
  222. if hasattr(response, 'usage') and response.usage:
  223. usage = {
  224. 'prompt_tokens': response.usage.prompt_tokens,
  225. 'completion_tokens': response.usage.completion_tokens
  226. }
  227. input_tokens, output_tokens, total_cost = calculate_cost(usage, model=MODEL)
  228. logging.info(f"Coût de l'analyse de l'image : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens}")
  229. else:
  230. logging.warning("Informations d'utilisation non disponibles pour le calcul du coût.")
  231. return analysis
  232. else:
  233. return None
  234. except OpenAIError as e:
  235. logger.error(f"Erreur lors de l'analyse de l'image avec {str.capitalize(MODEL)} : {e}")
  236. return None
  237. async def remove_old_image_analyses(new_analysis=False):
  238. global conversation_history, last_analysis_index, messages_since_last_analysis
  239. if new_analysis:
  240. logger.debug("Nouvelle analyse détectée. Suppression des anciennes analyses.")
  241. # Lorsqu'une nouvelle analyse est ajoutée, supprimer toutes les anciennes
  242. conversation_history = [
  243. msg for msg in conversation_history
  244. if not (msg.get("role") == "system" and msg.get("content", "").startswith("__IMAGE_ANALYSIS__:"))
  245. ]
  246. last_analysis_index = len(conversation_history)
  247. messages_since_last_analysis = 0
  248. save_conversation_history()
  249. logger.info("Toutes les anciennes analyses d'image ont été supprimées.")
  250. else:
  251. if last_analysis_index is not None:
  252. messages_since_last_analysis += 1
  253. if messages_since_last_analysis > 15:
  254. # Supprimer l'analyse
  255. if 0 <= last_analysis_index < len(conversation_history):
  256. removed_msg = conversation_history.pop(last_analysis_index)
  257. logger.info(f"Analyse d'image supprimée de l'historique : {removed_msg.get('content')[:50]}...")
  258. last_analysis_index = None
  259. messages_since_last_analysis = 0
  260. save_conversation_history()
  261. async def call_gpt4o_mini_with_analysis(analysis_text, user_name, user_question, has_text):
  262. try:
  263. # Préparer le message avec le prompt de personnalité et l'analyse
  264. messages = [
  265. {"role": "system", "content": PERSONALITY_PROMPT},
  266. {
  267. "role": "system",
  268. "content": f"L'analyse de l'image fournie est la suivante :\n{analysis_text}\n\n"
  269. }
  270. ]
  271. if has_text:
  272. # Préparer le message utilisateur avec le texte
  273. user_message = {
  274. "role": "user",
  275. "content": (
  276. f"Tu es {BOT_NAME}, une femme de 24 ans et membre à part entière du serveur Discord sur lequel tu te trouves."
  277. f"{user_name} a posté un message contenant une image et a écrit avec : '{user_question}'.\n"
  278. "Réponds à l'utilisateur en te basant sur l'analyse, avec ta personnalité"
  279. "Ne mentionne pas explicitement que l'analyse est pré-existante, fais comme si tu l'avais faite toi-même."
  280. )
  281. }
  282. else:
  283. # Préparer une instruction pour commenter l'image sans texte
  284. user_message = {
  285. "role": "user",
  286. "content": (
  287. f"Tu es {BOT_NAME}, une femme de 24 ans et membre à part entière du serveur Discord sur lequel tu te trouves."
  288. f"{user_name} a partagé une image sans texte additionnel.\n"
  289. "Commente l'image en te basant sur l'analyse, avec ta personnalité. "
  290. "Ne mentionne pas que l'analyse a été fournie à l'avance, réagis comme si tu l'avais toi-même effectuée."
  291. )
  292. }
  293. # Inclure l'historique de conversation avant d'ajouter le message utilisateur
  294. messages += conversation_history
  295. messages.append(user_message)
  296. # Appel à GPT-4o Mini pour répondre
  297. response = await openai_client.chat.completions.create(
  298. model=MODEL,
  299. messages=messages,
  300. max_tokens=450
  301. )
  302. if response:
  303. reply = response.choices[0].message.content
  304. # Calculer et enregistrer le coût de la réponse de GPT-4o Mini
  305. if hasattr(response, 'usage') and response.usage:
  306. usage = {
  307. 'prompt_tokens': response.usage.prompt_tokens,
  308. 'completion_tokens': response.usage.completion_tokens
  309. }
  310. input_tokens, output_tokens, total_cost = calculate_cost(usage, model=MODEL)
  311. logging.info(f"Coût de la réponse de {str.capitalize(MODEL)} : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens}")
  312. else:
  313. logging.warning(f"Informations d'utilisation non disponibles pour le calcul du coût de {str.capitalize(MODEL)}")
  314. return reply
  315. else:
  316. return None
  317. except OpenAIError as e:
  318. logger.error(f"Erreur lors de la génération de réponse avec {str.capitalize(MODEL)} : {e}")
  319. return None
  320. async def read_text_file(attachment):
  321. file_bytes = await attachment.read()
  322. return file_bytes.decode('utf-8')
  323. async def encode_image_from_attachment(attachment, mode='high'):
  324. image_data = await attachment.read()
  325. resized_image = resize_image(image_data, mode=mode, attachment_filename=attachment.filename)
  326. return base64.b64encode(resized_image).decode('utf-8')
  327. async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
  328. # Préparer le contenu pour l'appel API
  329. message_to_send = {
  330. "role": "user",
  331. "content": [
  332. {"type": "text", "text": f"{user_name} dit : {user_text}"}
  333. ]
  334. }
  335. # Inclure l'image dans l'appel API courant
  336. if image_data:
  337. message_to_send["content"].append({
  338. "type": "image_url",
  339. "image_url": {
  340. "url": f"data:image/jpeg;base64,{image_data}",
  341. "detail": detail
  342. }
  343. })
  344. # Assembler les messages avec le prompt de personnalité en premier
  345. messages = [
  346. {"role": "system", "content": PERSONALITY_PROMPT}
  347. ] + conversation_history + [message_to_send]
  348. try:
  349. response = await openai_client.chat.completions.create(
  350. model=MODEL,
  351. messages=messages,
  352. max_tokens=400,
  353. temperature=TEMPERATURE
  354. )
  355. if response:
  356. reply = response.choices[0].message.content
  357. # Ajouter le message de l'utilisateur à l'historique global, mais uniquement s'il ne s'agit pas d'une image
  358. if image_data is None:
  359. await add_to_conversation_history(message_to_send)
  360. # Ajouter la réponse de l'IA directement à l'historique
  361. await add_to_conversation_history({
  362. "role": "assistant",
  363. "content": reply
  364. })
  365. if hasattr(response, 'usage') and response.usage:
  366. usage = response.usage
  367. input_tokens, output_tokens, total_cost = calculate_cost({
  368. 'prompt_tokens': usage.prompt_tokens,
  369. 'completion_tokens': usage.completion_tokens
  370. })
  371. # Afficher dans la console
  372. logging.info("Réponse envoyée.")
  373. #logging.info(f"Coût de la réponse : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens} / Total: {input_tokens + output_tokens}")
  374. return response
  375. except OpenAIError as e:
  376. logger.error(f"Erreur durant l'appel de l'API : {e}")
  377. except Exception as e:
  378. logger.error(f"Erreur durant l'appel de l'API : {e}")
  379. return None
  380. @client_discord.event
  381. async def on_ready():
  382. logger.info(f'{BOT_NAME} connecté en tant que {client_discord.user}')
  383. logger.info(f'Utilisation du modèle {MODEL}')
  384. if not conversation_history:
  385. logger.info("Aucun historique trouvé. L'historique commence vide.")
  386. # Envoyer un message de version dans le canal Discord
  387. channel = client_discord.get_channel(chatgpt_channel_id)
  388. if channel:
  389. try:
  390. embed = discord.Embed(
  391. title=f"Bot Démarré",
  392. description=f"🎉 {BOT_NAME} est en ligne ! Version {BOT_VERSION}\nUtilisation du modèle: **{MODEL}**",
  393. color=0x2222aa # Bleu
  394. )
  395. await channel.send(embed=embed)
  396. logger.info(f"Message de connexion envoyé dans le canal ID {chatgpt_channel_id}")
  397. except discord.Forbidden:
  398. logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal ID {chatgpt_channel_id}.")
  399. except discord.HTTPException as e:
  400. logger.error(f"Erreur lors de l'envoi du message de connexion : {e}")
  401. else:
  402. logger.error(f"Canal avec ID {chatgpt_channel_id} non trouvé.")
  403. @client_discord.event
  404. async def on_message(message):
  405. global conversation_history, last_analysis_index, messages_since_last_analysis
  406. # Vérifier si le message provient du canal autorisé
  407. if message.channel.id != chatgpt_channel_id:
  408. return
  409. # Ignorer les messages du bot lui-même
  410. if message.author == client_discord.user:
  411. return
  412. user_text = message.content.strip()
  413. image_data = None
  414. file_content = None
  415. attachment_filename = None
  416. # Vérifier si le message est la commande de réinitialisation
  417. if user_text.lower() == "!reset_history":
  418. # Vérifier si l'utilisateur a les permissions administratives
  419. if not message.author.guild_permissions.administrator:
  420. await message.channel.send("❌ Vous n'avez pas la permission d'utiliser cette commande.")
  421. return
  422. conversation_history = []
  423. save_conversation_history()
  424. await message.channel.send("✅ L'historique des conversations a été réinitialisé.")
  425. logger.info(f"Historique des conversations réinitialisé par {message.author}.")
  426. return # Arrêter le traitement du message après la réinitialisation
  427. # Extensions de fichiers autorisées
  428. allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
  429. # Variables pour stocker si le message contient une image et/ou un fichier
  430. has_image = False
  431. has_file = False
  432. # Vérifier s'il y a une pièce jointe
  433. if message.attachments:
  434. for attachment in message.attachments:
  435. # Vérifier si c'est un fichier avec une extension autorisée
  436. if any(attachment.filename.endswith(ext) for ext in allowed_extensions):
  437. file_content = await read_text_file(attachment)
  438. attachment_filename = attachment.filename
  439. break
  440. # Vérifier si c'est une image
  441. elif attachment.content_type in ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff']:
  442. image_data = await encode_image_from_attachment(attachment, mode='high')
  443. break
  444. # Si une image est présente, la traiter
  445. if image_data:
  446. has_user_text = has_text(user_text)
  447. user_text_to_use = user_text if has_user_text else None
  448. # **Étape 1 : Envoyer un message temporaire indiquant que l'image est en cours d'analyse**
  449. temp_msg = await message.channel.send(f"*{BOT_NAME} observe l'image...*")
  450. try:
  451. # Étape 2 : GPT-4o analyse l'image, potentiellement guidée par le texte de l'utilisateur
  452. analysis = await call_gpt4o_for_image_analysis(image_data, user_text=user_text_to_use)
  453. if analysis:
  454. # **Ajouter l'analyse à l'historique avant de réagir avec GPT-4o Mini**
  455. analysis_message = {
  456. "role": "system",
  457. "content": f"__IMAGE_ANALYSIS__:{analysis}"
  458. }
  459. await add_to_conversation_history(analysis_message)
  460. # Mettre à jour l'index de la dernière analyse
  461. last_analysis_index = len(conversation_history) - 1
  462. messages_since_last_analysis = 0
  463. # Étape 3 : GPT-4o Mini réagit à la question et à l'analyse
  464. reply = await call_gpt4o_mini_with_analysis(analysis, message.author.name, user_text, has_user_text)
  465. if reply:
  466. # **Étape 4 : Supprimer le message temporaire**
  467. await temp_msg.delete()
  468. # **Étape 5 : Envoyer la réponse finale**
  469. await message.channel.send(reply.rstrip("</s>"))
  470. # **Ajout des messages à l'historique**
  471. # Créer un message utilisateur modifié indiquant qu'une image a été postée
  472. if has_user_text:
  473. user_message_content = f"{user_text} (a posté une image.)"
  474. else:
  475. user_message_content = (
  476. "Une image a été postée, mais elle n'est pas disponible pour analyse directe. "
  477. "Veuillez vous baser uniquement sur l'analyse fournie."
  478. )
  479. user_message = {
  480. "role": "user",
  481. "content": user_message_content
  482. }
  483. # Ajouter le message utilisateur à l'historique
  484. await add_to_conversation_history(user_message)
  485. # Créer le message assistant avec la réponse de GPT-4o Mini
  486. assistant_message = {
  487. "role": "assistant",
  488. "content": reply
  489. }
  490. # Ajouter le message assistant à l'historique
  491. await add_to_conversation_history(assistant_message)
  492. else:
  493. # **Étape 4 : Supprimer le message temporaire en cas d'échec de génération de réponse**
  494. await temp_msg.delete()
  495. await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
  496. else:
  497. # **Étape 4 : Supprimer le message temporaire en cas d'échec d'analyse**
  498. await temp_msg.delete()
  499. await message.channel.send("Désolé, je n'ai pas pu analyser l'image.")
  500. except Exception as e:
  501. # **Étape 4 : Supprimer le message temporaire en cas d'erreur**
  502. await temp_msg.delete()
  503. await message.channel.send("Une erreur est survenue lors du traitement de l'image.")
  504. logger.error(f"Error during image processing: {e}")
  505. # Après traitement de l'image, ne pas continuer
  506. return
  507. # Ajouter le contenu du fichier à la requête si présent
  508. if file_content:
  509. user_text += f"\nContenu du fichier {attachment.filename}:\n{file_content}"
  510. # Vérifier si le texte n'est pas vide après ajout du contenu du fichier
  511. if not has_text(user_text):
  512. return # Ne pas appeler l'API si le texte est vide
  513. # Appeler l'API OpenAI
  514. result = await call_openai_api(user_text, message.author.name, image_data)
  515. if result:
  516. reply = result.choices[0].message.content
  517. reply = reply.rstrip("</s>")
  518. if len(reply.split('</think>')) > 1:
  519. reply = reply.split('</think>')[1]
  520. elif len(reply.split('</response>')) > 1:
  521. reply = reply.split('</response>')[1]
  522. await message.channel.send(reply)
  523. async def add_to_conversation_history(new_message):
  524. global conversation_history, last_analysis_index, messages_since_last_analysis
  525. # Ne pas ajouter le PERSONALITY_PROMPT à l'historique
  526. if new_message.get("role") == "system" and new_message.get("content") == PERSONALITY_PROMPT:
  527. logger.debug("PERSONALITY_PROMPT système non ajouté à l'historique.")
  528. return
  529. if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
  530. await remove_old_image_analyses(new_analysis=True)
  531. conversation_history.append(new_message)
  532. save_conversation_history()
  533. logger.debug(f"Message ajouté à l'historique. Taille actuelle : {len(conversation_history)}")
  534. # Gérer la suppression des analyses d'images après 15 messages
  535. if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
  536. last_analysis_index = len(conversation_history) - 1
  537. messages_since_last_analysis = 0
  538. else:
  539. await remove_old_image_analyses(new_analysis=False)
  540. if len(conversation_history) > 50:
  541. logger.info("Limite de 50 messages atteinte.")
  542. excess_messages = len(conversation_history) - 50
  543. if excess_messages > 0:
  544. # Supprimer les messages les plus anciens
  545. del conversation_history[:excess_messages]
  546. save_conversation_history()
  547. logger.info(f"{excess_messages} messages les plus anciens ont été supprimés pour maintenir l'historique à 50 messages.")
  548. # Démarrer le bot Discord
  549. client_discord.run(DISCORD_TOKEN)