chatbot.py 25 KB

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