chatbot.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. import os
  2. import json
  3. import logging
  4. import base64
  5. from io import BytesIO
  6. import asyncio
  7. import mysql.connector
  8. from mysql.connector import Error
  9. from PIL import Image
  10. import tiktoken
  11. import discord
  12. from discord.ext import commands
  13. from dotenv import load_dotenv
  14. from openai import AsyncOpenAI, OpenAIError
  15. # =================================
  16. # Configuration et Initialisation
  17. # =================================
  18. # Charger les variables d'environnement depuis le fichier .env
  19. load_dotenv()
  20. DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
  21. OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
  22. DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
  23. PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
  24. IMAGE_ANALYSIS_PROMPT_FILE = os.getenv('IMAGE_ANALYSIS_PROMPT_FILE', 'image_analysis_prompt.txt')
  25. BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
  26. BOT_VERSION = "2.7.0"
  27. # Validation des variables d'environnement
  28. required_env_vars = {
  29. 'DISCORD_TOKEN': DISCORD_TOKEN,
  30. 'OPENAI_API_KEY': OPENAI_API_KEY,
  31. 'DISCORD_CHANNEL_ID': DISCORD_CHANNEL_ID,
  32. 'IMAGE_ANALYSIS_PROMPT_FILE': IMAGE_ANALYSIS_PROMPT_FILE
  33. }
  34. missing_vars = [var for var, val in required_env_vars.items() if val is None]
  35. if missing_vars:
  36. raise ValueError(f"Les variables d'environnement suivantes ne sont pas définies: {', '.join(missing_vars)}")
  37. # Vérification de l'existence des fichiers de prompt
  38. for file_var, file_path in [('PERSONALITY_PROMPT_FILE', PERSONALITY_PROMPT_FILE),
  39. ('IMAGE_ANALYSIS_PROMPT_FILE', IMAGE_ANALYSIS_PROMPT_FILE)]:
  40. if not os.path.isfile(file_path):
  41. raise FileNotFoundError(f"Le fichier de prompt '{file_var}' '{file_path}' est introuvable.")
  42. # Lire les prompts depuis les fichiers
  43. with open(PERSONALITY_PROMPT_FILE, 'r', encoding='utf-8') as f:
  44. PERSONALITY_PROMPT = f.read().strip()
  45. with open(IMAGE_ANALYSIS_PROMPT_FILE, 'r', encoding='utf-8') as f:
  46. IMAGE_ANALYSIS_PROMPT = f.read().strip()
  47. # Configurer les logs
  48. LOG_FORMAT = '%(asctime)s : %(name)s : %(levelname)s : %(message)s'
  49. logging.basicConfig(
  50. handlers=[
  51. logging.FileHandler("./chatbot.log", mode='a', encoding='utf-8'),
  52. logging.StreamHandler()
  53. ],
  54. format=LOG_FORMAT,
  55. level=logging.INFO
  56. )
  57. logger = logging.getLogger(BOT_NAME)
  58. logger.setLevel(logging.INFO)
  59. logging.getLogger('httpx').setLevel(logging.WARNING) # Réduire le niveau de log pour 'httpx'
  60. # Initialiser les intents Discord
  61. intents = discord.Intents.default()
  62. intents.message_content = True
  63. # Initialiser le client OpenAI asynchrone
  64. openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
  65. # Convertir l'ID du canal Discord en entier
  66. try:
  67. chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
  68. except ValueError:
  69. raise ValueError("L'ID du canal Discord est invalide. Assurez-vous qu'il s'agit d'un entier.")
  70. # =====================================
  71. # Gestion de la Base de Données MariaDB
  72. # =====================================
  73. class DatabaseManager:
  74. def __init__(self):
  75. self.connection = self.create_db_connection()
  76. def create_db_connection(self):
  77. try:
  78. connection = mysql.connector.connect(
  79. host=os.getenv('DB_HOST'),
  80. user=os.getenv('DB_USER'),
  81. password=os.getenv('DB_PASSWORD'),
  82. database=os.getenv('DB_NAME'),
  83. charset='utf8mb4',
  84. collation='utf8mb4_unicode_ci'
  85. )
  86. if connection.is_connected():
  87. logger.info("Connexion réussie à MariaDB")
  88. return connection
  89. except Error as e:
  90. logger.error(f"Erreur de connexion à MariaDB: {e}")
  91. return None
  92. def load_conversation_history(self):
  93. global conversation_history
  94. try:
  95. with self.connection.cursor(dictionary=True) as cursor:
  96. cursor.execute("SELECT role, content FROM conversation_history ORDER BY id ASC")
  97. rows = cursor.fetchall()
  98. conversation_history = [
  99. row for row in rows
  100. if not (row['role'] == "system" and row['content'] == PERSONALITY_PROMPT)
  101. ]
  102. logger.info("Historique chargé depuis la base de données")
  103. except Error as e:
  104. logger.error(f"Erreur lors du chargement de l'historique depuis la base de données: {e}")
  105. conversation_history = []
  106. def save_message(self, role, content):
  107. try:
  108. with self.connection.cursor() as cursor:
  109. sql = "INSERT INTO conversation_history (role, content) VALUES (%s, %s)"
  110. cursor.execute(sql, (role, json.dumps(content, ensure_ascii=False) if isinstance(content, (dict, list)) else content))
  111. self.connection.commit()
  112. logger.debug(f"Message sauvegardé dans la base de données: {role} - {content[:50]}...")
  113. except Error as e:
  114. logger.error(f"Erreur lors de la sauvegarde du message dans la base de données: {e}")
  115. def delete_old_image_analyses(self):
  116. try:
  117. with self.connection.cursor() as cursor:
  118. cursor.execute("DELETE FROM conversation_history WHERE role = 'system' AND content LIKE '__IMAGE_ANALYSIS__:%'")
  119. self.connection.commit()
  120. logger.info("Toutes les anciennes analyses d'image ont été supprimées de la base de données.")
  121. except Error as e:
  122. logger.error(f"Erreur lors de la suppression des analyses d'image: {e}")
  123. def reset_history(self):
  124. try:
  125. with self.connection.cursor() as cursor:
  126. cursor.execute("DELETE FROM conversation_history")
  127. self.connection.commit()
  128. logger.info("Historique des conversations réinitialisé.")
  129. except Error as e:
  130. logger.error(f"Erreur lors de la réinitialisation de l'historique: {e}")
  131. def delete_old_messages(self, limit):
  132. try:
  133. with self.connection.cursor() as cursor:
  134. cursor.execute("DELETE FROM conversation_history ORDER BY id ASC LIMIT %s", (limit,))
  135. self.connection.commit()
  136. logger.debug(f"{limit} messages les plus anciens ont été supprimés de la base de données pour maintenir l'historique à 150 messages.")
  137. except Error as e:
  138. logger.error(f"Erreur lors de la suppression des anciens messages: {e}")
  139. def close_connection(self):
  140. if self.connection and self.connection.is_connected():
  141. self.connection.close()
  142. logger.info("Connexion à la base de données fermée.")
  143. # ===============================
  144. # Gestion de l'Historique des Messages
  145. # ===============================
  146. conversation_history = []
  147. last_analysis_index = None
  148. messages_since_last_analysis = 0
  149. # ====================
  150. # Fonctions Utilitaires
  151. # ====================
  152. def has_text(text):
  153. """Détermine si le texte fourni est non vide après suppression des espaces."""
  154. return bool(text.strip())
  155. def resize_image(image_bytes, mode='high', attachment_filename=None):
  156. """Redimensionne l'image selon le mode spécifié."""
  157. try:
  158. with Image.open(BytesIO(image_bytes)) as img:
  159. original_format = img.format # Stocker le format original
  160. if mode == 'high':
  161. img.thumbnail((2000, 2000))
  162. if min(img.size) < 768:
  163. scale = 768 / min(img.size)
  164. new_size = tuple(int(x * scale) for x in img.size)
  165. img = img.resize(new_size, Image.Resampling.LANCZOS)
  166. elif mode == 'low':
  167. img = img.resize((512, 512))
  168. buffer = BytesIO()
  169. img_format = img.format or _infer_image_format(attachment_filename)
  170. img.save(buffer, format=img_format)
  171. return buffer.getvalue()
  172. except Exception as e:
  173. logger.error(f"Erreur lors du redimensionnement de l'image : {e}")
  174. raise
  175. def _infer_image_format(filename):
  176. """Déduit le format de l'image basé sur l'extension du fichier."""
  177. if filename:
  178. _, ext = os.path.splitext(filename)
  179. ext = ext.lower()
  180. format_mapping = {
  181. '.jpg': 'JPEG',
  182. '.jpeg': 'JPEG',
  183. '.png': 'PNG',
  184. '.gif': 'GIF',
  185. '.bmp': 'BMP',
  186. '.tiff': 'TIFF'
  187. }
  188. return format_mapping.get(ext, 'PNG')
  189. return 'PNG'
  190. def extract_text_from_message(message):
  191. """Extrait le texte du message."""
  192. content = message.get("content", "")
  193. if isinstance(content, list):
  194. texts = [part.get("text", "") for part in content if isinstance(part, dict) and part.get("text")]
  195. return ' '.join(texts)
  196. elif isinstance(content, str):
  197. return content
  198. return ""
  199. def calculate_cost(usage, model='gpt-4o-mini'):
  200. """Calcule le coût basé sur l'utilisation des tokens."""
  201. input_tokens = usage.get('prompt_tokens', 0)
  202. output_tokens = usage.get('completion_tokens', 0)
  203. model_costs = {
  204. 'gpt-4o': {
  205. 'input_rate': 5.00 / 1_000_000, # 5$ pour 1M tokens d'entrée
  206. 'output_rate': 15.00 / 1_000_000 # 15$ pour 1M tokens de sortie
  207. },
  208. 'gpt-4o-mini': {
  209. 'input_rate': 0.150 / 1_000_000, # 0.150$ pour 1M tokens d'entrée
  210. 'output_rate': 0.600 / 1_000_000 # 0.600$ pour 1M tokens de sortie
  211. }
  212. }
  213. rates = model_costs.get(model, model_costs['gpt-4o-mini'])
  214. input_cost = input_tokens * rates['input_rate']
  215. output_cost = output_tokens * rates['output_rate']
  216. total_cost = input_cost + output_cost
  217. if model not in model_costs:
  218. logger.warning(f"Modèle inconnu '{model}'. Utilisation des tarifs par défaut pour 'gpt-4o-mini'.")
  219. return input_tokens, output_tokens, total_cost
  220. async def read_text_file(attachment):
  221. """Lit le contenu d'un fichier texte attaché."""
  222. file_bytes = await attachment.read()
  223. return file_bytes.decode('utf-8')
  224. async def encode_image_from_attachment(attachment, mode='high'):
  225. """Encode une image depuis une pièce jointe en base64 après redimensionnement."""
  226. image_data = await attachment.read()
  227. resized_image = resize_image(image_data, mode=mode, attachment_filename=attachment.filename)
  228. return base64.b64encode(resized_image).decode('utf-8')
  229. # =================================
  230. # Interaction avec OpenAI
  231. # =================================
  232. # Charger l'encodeur pour le modèle GPT-4o mini
  233. encoding = tiktoken.get_encoding("o200k_base")
  234. async def call_openai_model(model, messages, max_tokens, temperature=0.8):
  235. """Appelle un modèle OpenAI avec les paramètres spécifiés et gère la réponse."""
  236. try:
  237. response = await openai_client.chat.completions.create(
  238. model=model,
  239. messages=messages,
  240. max_tokens=max_tokens,
  241. temperature=temperature
  242. )
  243. if response and response.choices:
  244. reply = response.choices[0].message.content
  245. # Ne pas logger les réponses de 'gpt-4o-mini' et 'gpt-4o'
  246. if model not in ["gpt-4o-mini", "gpt-4o"]:
  247. logger.info(f"Réponse de {model}: {reply[:100]}...")
  248. if hasattr(response, 'usage') and response.usage:
  249. usage = {
  250. 'prompt_tokens': response.usage.prompt_tokens,
  251. 'completion_tokens': response.usage.completion_tokens
  252. }
  253. _, _, total_cost = calculate_cost(usage, model=model)
  254. # Log avec les tokens d'entrée et de sortie
  255. logger.info(f"Coût de l'utilisation de {model}: ${total_cost:.4f} / Input: {usage['prompt_tokens']} / Output: {usage['completion_tokens']}")
  256. else:
  257. logger.warning(f"Informations d'utilisation non disponibles pour {model}.")
  258. return reply
  259. except OpenAIError as e:
  260. logger.error(f"Erreur lors de l'appel à l'API OpenAI avec {model}: {e}")
  261. except Exception as e:
  262. logger.error(f"Erreur inattendue lors de l'appel à l'API OpenAI avec {model}: {e}")
  263. return None
  264. async def call_gpt4o_for_image_analysis(image_data, user_text=None, detail='high'):
  265. """Appelle GPT-4o pour analyser une image."""
  266. prompt = IMAGE_ANALYSIS_PROMPT
  267. if user_text:
  268. prompt += f" Voici ce que l'on te décrit : \"{user_text}\"."
  269. message_to_send = {
  270. "role": "user",
  271. "content": [
  272. {"type": "text", "text": prompt},
  273. {
  274. "type": "image_url",
  275. "image_url": {
  276. "url": f"data:image/jpeg;base64,{image_data}",
  277. "detail": detail
  278. }
  279. }
  280. ]
  281. }
  282. messages = [message_to_send]
  283. analysis = await call_openai_model(
  284. model="gpt-4o",
  285. messages=messages,
  286. max_tokens=4096,
  287. temperature=1.0
  288. )
  289. if analysis:
  290. logger.info(f"Analyse de l'image par GPT-4o : {analysis}")
  291. return analysis
  292. async def call_gpt4o_mini_with_analysis(analysis_text, user_name, user_question, has_text_flag):
  293. """Appelle GPT-4o Mini pour générer une réponse basée sur l'analyse de l'image."""
  294. system_messages = [
  295. {"role": "system", "content": PERSONALITY_PROMPT},
  296. {
  297. "role": "system",
  298. "content": f"L'analyse de l'image fournie est la suivante :\n{analysis_text}\n\n"
  299. }
  300. ]
  301. if has_text_flag:
  302. user_content = (
  303. f"{user_name} a posté un message contenant une image et a écrit avec : '{user_question}'. "
  304. "Réponds à l'utilisateur en te basant sur l'analyse, avec ta personnalité. "
  305. "Ne mentionne pas explicitement que l'analyse est pré-existante, fais comme si tu l'avais faite toi-même."
  306. )
  307. else:
  308. user_content = (
  309. f"{user_name} a partagé une image sans texte additionnel. "
  310. "Commente l'image en te basant sur l'analyse, avec ta personnalité. "
  311. "Ne mentionne pas que l'analyse a été fournie à l'avance, réagis comme si tu l'avais toi-même effectuée."
  312. )
  313. user_message = {"role": "user", "content": user_content}
  314. messages = system_messages + conversation_history + [user_message]
  315. reply = await call_openai_model(
  316. model="gpt-4o-mini",
  317. messages=messages,
  318. max_tokens=450,
  319. temperature=1.0
  320. )
  321. return reply
  322. async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
  323. """Appelle l'API OpenAI pour générer une réponse basée sur le texte et/ou l'image."""
  324. text = f"{user_name} dit : {user_text}"
  325. if image_data:
  326. text += " (a posté une image.)"
  327. message_to_send = {
  328. "role": "user",
  329. "content": [
  330. {"type": "text", "text": text}
  331. ]
  332. }
  333. if image_data:
  334. message_to_send["content"].append({
  335. "type": "image_url",
  336. "image_url": {
  337. "url": f"data:image/jpeg;base64,{image_data}",
  338. "detail": detail
  339. }
  340. })
  341. messages = [
  342. {"role": "system", "content": PERSONALITY_PROMPT}
  343. ] + conversation_history + [message_to_send]
  344. reply = await call_openai_model(
  345. model="gpt-4o-mini",
  346. messages=messages,
  347. max_tokens=450,
  348. temperature=1.0
  349. )
  350. return reply
  351. # =====================================
  352. # Gestion du Contenu de l'Historique
  353. # =====================================
  354. async def remove_old_image_analyses(db_manager, new_analysis=False):
  355. """Supprime les anciennes analyses d'images de l'historique."""
  356. global conversation_history, last_analysis_index, messages_since_last_analysis
  357. if new_analysis:
  358. logger.debug("Nouvelle analyse détectée. Suppression des anciennes analyses.")
  359. conversation_history = [
  360. msg for msg in conversation_history
  361. if not (msg.get("role") == "system" and msg.get("content", "").startswith("__IMAGE_ANALYSIS__:"))
  362. ]
  363. last_analysis_index = len(conversation_history)
  364. messages_since_last_analysis = 0
  365. # Supprimer les analyses d'images de la base de données
  366. db_manager.delete_old_image_analyses()
  367. else:
  368. # Exemple de logique additionnelle si nécessaire
  369. pass
  370. async def add_to_conversation_history(db_manager, new_message):
  371. global conversation_history, last_analysis_index, messages_since_last_analysis
  372. # Exclure le PERSONALITY_PROMPT de l'historique
  373. if new_message.get("role") == "system" and new_message.get("content") == PERSONALITY_PROMPT:
  374. logger.debug("PERSONALITY_PROMPT système non ajouté à l'historique.")
  375. return
  376. # Gérer les analyses d'images
  377. if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
  378. await remove_old_image_analyses(db_manager, new_analysis=True)
  379. # Ajouter le message à l'historique en mémoire
  380. conversation_history.append(new_message)
  381. # Sauvegarder dans la base de données
  382. db_manager.save_message(new_message.get("role"), new_message.get("content"))
  383. logger.debug(f"Message ajouté à l'historique. Taille actuelle : {len(conversation_history)}")
  384. # Mettre à jour les indices pour les analyses d'images
  385. if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
  386. last_analysis_index = len(conversation_history) - 1
  387. messages_since_last_analysis = 0
  388. else:
  389. await remove_old_image_analyses(db_manager, new_analysis=False)
  390. # Limiter l'historique à 150 messages
  391. if len(conversation_history) > 150:
  392. excess = len(conversation_history) - 150
  393. conversation_history = conversation_history[excess:]
  394. # Supprimer les messages les plus anciens de la base de données
  395. db_manager.delete_old_messages(excess)
  396. # =====================================
  397. # Gestion des Événements Discord
  398. # =====================================
  399. class MyDiscordClient(discord.Client):
  400. def __init__(self, db_manager, **options):
  401. super().__init__(**options)
  402. self.db_manager = db_manager
  403. self.message_queue = asyncio.Queue()
  404. async def setup_hook(self):
  405. """Hook d'initialisation asynchrone pour configurer des tâches supplémentaires."""
  406. self.processing_task = asyncio.create_task(self.process_messages())
  407. async def close(self):
  408. if openai_client:
  409. await openai_client.close()
  410. self.db_manager.close_connection()
  411. self.processing_task.cancel()
  412. await super().close()
  413. async def on_ready(self):
  414. """Événement déclenché lorsque le bot est prêt."""
  415. logger.info(f'{BOT_NAME} connecté en tant que {self.user}')
  416. if not conversation_history:
  417. logger.info("Aucun historique trouvé. L'historique commence vide.")
  418. # Envoyer un message de version dans le canal Discord
  419. channel = self.get_channel(chatgpt_channel_id)
  420. if channel:
  421. try:
  422. embed = discord.Embed(
  423. title="Bot Démarré",
  424. description=f"🎉 {BOT_NAME} est en ligne ! Version {BOT_VERSION}",
  425. color=0x00ff00 # Vert
  426. )
  427. await channel.send(embed=embed)
  428. logger.info(f"Message de connexion envoyé dans le canal ID {chatgpt_channel_id}")
  429. except discord.Forbidden:
  430. logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal ID {chatgpt_channel_id}.")
  431. except discord.HTTPException as e:
  432. logger.error(f"Erreur lors de l'envoi du message de connexion : {e}")
  433. else:
  434. logger.error(f"Canal avec ID {chatgpt_channel_id} non trouvé.")
  435. async def on_message(self, message):
  436. """Événement déclenché lorsqu'un message est envoyé dans un canal suivi."""
  437. # Ignorer les messages provenant d'autres canaux ou du bot lui-même
  438. if message.channel.id != chatgpt_channel_id or message.author == self.user:
  439. return
  440. await self.message_queue.put(message)
  441. async def process_messages(self):
  442. """Tâche en arrière-plan pour traiter les messages séquentiellement."""
  443. while True:
  444. message = await self.message_queue.get()
  445. try:
  446. await self.handle_message(message)
  447. except Exception as e:
  448. logger.error(f"Erreur lors du traitement du message : {e}")
  449. try:
  450. await message.channel.send("Une erreur est survenue lors du traitement de votre message.")
  451. except Exception as send_error:
  452. logger.error(f"Erreur lors de l'envoi du message d'erreur : {send_error}")
  453. finally:
  454. self.message_queue.task_done()
  455. async def handle_message(self, message):
  456. """Fonction pour traiter un seul message."""
  457. global conversation_history, last_analysis_index, messages_since_last_analysis
  458. user_text = message.content.strip()
  459. # Commande de réinitialisation de l'historique
  460. if user_text.lower() == "!reset_history":
  461. if not message.author.guild_permissions.administrator:
  462. await message.channel.send("❌ Vous n'avez pas la permission d'utiliser cette commande.")
  463. return
  464. conversation_history = []
  465. self.db_manager.reset_history()
  466. await message.channel.send("✅ L'historique des conversations a été réinitialisé.")
  467. return
  468. # Traiter les pièces jointes
  469. image_data = None
  470. file_content = None
  471. attachment_filename = None
  472. allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
  473. if message.attachments:
  474. for attachment in message.attachments:
  475. if any(attachment.filename.lower().endswith(ext) for ext in allowed_extensions):
  476. file_content = await read_text_file(attachment)
  477. attachment_filename = attachment.filename
  478. break
  479. elif attachment.content_type and attachment.content_type.startswith('image/'):
  480. image_data = await encode_image_from_attachment(attachment, mode='high')
  481. break
  482. # Traitement des images
  483. if image_data:
  484. has_user_text = has_text(user_text)
  485. user_text_to_use = user_text if has_user_text else None
  486. temp_msg = await message.channel.send(f"*{BOT_NAME} observe l'image...*")
  487. try:
  488. # Analyser l'image avec GPT-4o
  489. analysis = await call_gpt4o_for_image_analysis(image_data, user_text=user_text_to_use)
  490. if analysis:
  491. # Ajouter l'analyse à l'historique
  492. analysis_message = {
  493. "role": "system",
  494. "content": f"__IMAGE_ANALYSIS__:{analysis}"
  495. }
  496. await add_to_conversation_history(self.db_manager, analysis_message)
  497. # Générer une réponse basée sur l'analyse
  498. reply = await call_gpt4o_mini_with_analysis(analysis, message.author.name, user_text, has_user_text)
  499. if reply:
  500. await temp_msg.delete()
  501. await message.channel.send(reply)
  502. # Construire et ajouter les messages à l'historique
  503. user_message_text = f"{user_text} (a posté une image.)" if has_user_text else (
  504. "Une image a été postée, mais elle n'est pas disponible pour analyse directe. Veuillez vous baser uniquement sur l'analyse fournie."
  505. )
  506. user_message = {
  507. "role": "user",
  508. "content": f"{message.author.name} dit : {user_message_text}"
  509. }
  510. assistant_message = {
  511. "role": "assistant",
  512. "content": reply
  513. }
  514. await add_to_conversation_history(self.db_manager, user_message)
  515. await add_to_conversation_history(self.db_manager, assistant_message)
  516. else:
  517. await temp_msg.delete()
  518. await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
  519. else:
  520. await temp_msg.delete()
  521. await message.channel.send("Désolé, je n'ai pas pu analyser l'image.")
  522. except Exception as e:
  523. await temp_msg.delete()
  524. await message.channel.send("Une erreur est survenue lors du traitement de l'image.")
  525. logger.error(f"Erreur lors du traitement de l'image: {e}")
  526. return # Ne pas continuer le traitement après une image
  527. # Ajouter le contenu du fichier au texte de l'utilisateur si un fichier est présent
  528. if file_content:
  529. user_text += f"\nContenu du fichier {attachment_filename}:\n{file_content}"
  530. # Vérifier si le texte n'est pas vide
  531. if not has_text(user_text):
  532. return # Ne pas appeler l'API si le texte est vide
  533. async with message.channel.typing():
  534. try:
  535. # Appeler l'API OpenAI pour le texte
  536. reply = await call_openai_api(user_text, message.author.name)
  537. if reply:
  538. await message.channel.send(reply)
  539. # Construire et ajouter les messages à l'historique
  540. user_message = {
  541. "role": "user",
  542. "content": f"{message.author.name} dit : {user_text}"
  543. }
  544. assistant_message = {
  545. "role": "assistant",
  546. "content": reply
  547. }
  548. await add_to_conversation_history(self.db_manager, user_message)
  549. await add_to_conversation_history(self.db_manager, assistant_message)
  550. else:
  551. await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
  552. except Exception as e:
  553. await message.channel.send("Une erreur est survenue lors de la génération de la réponse.")
  554. logger.error(f"Erreur lors du traitement du texte: {e}")
  555. # ============================
  556. # Démarrage du Bot Discord
  557. # ============================
  558. def main():
  559. db_manager = DatabaseManager()
  560. if not db_manager.connection:
  561. logger.error("Le bot ne peut pas démarrer sans connexion à la base de données.")
  562. return
  563. db_manager.load_conversation_history()
  564. client_discord = MyDiscordClient(db_manager=db_manager, intents=intents)
  565. try:
  566. client_discord.run(DISCORD_TOKEN)
  567. except Exception as e:
  568. logger.error(f"Erreur lors du démarrage du bot Discord: {e}")
  569. finally:
  570. db_manager.close_connection()
  571. if __name__ == "__main__":
  572. main()