|
@@ -1,12 +1,14 @@
|
|
|
import os
|
|
import os
|
|
|
-import openai
|
|
|
|
|
-import discord
|
|
|
|
|
-import aiohttp
|
|
|
|
|
-import asyncio
|
|
|
|
|
import base64
|
|
import base64
|
|
|
import logging
|
|
import logging
|
|
|
import re
|
|
import re
|
|
|
|
|
+from io import BytesIO
|
|
|
|
|
+import discord
|
|
|
from dotenv import load_dotenv
|
|
from dotenv import load_dotenv
|
|
|
|
|
+from PIL import Image
|
|
|
|
|
+import emoji
|
|
|
|
|
+import tiktoken
|
|
|
|
|
+from openai import AsyncOpenAI, OpenAIError
|
|
|
|
|
|
|
|
# Charger les variables d'environnement depuis le fichier .env
|
|
# Charger les variables d'environnement depuis le fichier .env
|
|
|
load_dotenv()
|
|
load_dotenv()
|
|
@@ -29,7 +31,7 @@ with open(PERSONALITY_PROMPT_FILE, 'r', encoding='utf-8') as f:
|
|
|
PERSONALITY_PROMPT = f.read().strip()
|
|
PERSONALITY_PROMPT = f.read().strip()
|
|
|
|
|
|
|
|
# Log configuration
|
|
# Log configuration
|
|
|
-log_format='%(asctime)-13s : %(name)-15s : %(levelname)-8s : %(message)s'
|
|
|
|
|
|
|
+log_format = '%(asctime)-13s : %(name)-15s : %(levelname)-8s : %(message)s'
|
|
|
logging.basicConfig(handlers=[logging.FileHandler("./chatbot.log", 'a', 'utf-8')], format=log_format, level="INFO")
|
|
logging.basicConfig(handlers=[logging.FileHandler("./chatbot.log", 'a', 'utf-8')], format=log_format, level="INFO")
|
|
|
|
|
|
|
|
console = logging.StreamHandler()
|
|
console = logging.StreamHandler()
|
|
@@ -41,51 +43,141 @@ logger.setLevel("INFO")
|
|
|
|
|
|
|
|
logging.getLogger('').addHandler(console)
|
|
logging.getLogger('').addHandler(console)
|
|
|
|
|
|
|
|
|
|
+httpx_logger = logging.getLogger('httpx')
|
|
|
|
|
+httpx_logger.setLevel(logging.WARNING)
|
|
|
|
|
+
|
|
|
# Initialiser les intents
|
|
# Initialiser les intents
|
|
|
intents = discord.Intents.default()
|
|
intents = discord.Intents.default()
|
|
|
intents.message_content = True # Activer l'intent pour les contenus de message
|
|
intents.message_content = True # Activer l'intent pour les contenus de message
|
|
|
|
|
|
|
|
-# Initialiser le client Discord avec les intents modifiés
|
|
|
|
|
-client_discord = discord.Client(intents=intents)
|
|
|
|
|
-
|
|
|
|
|
-# Initialiser l'API OpenAI avec un client
|
|
|
|
|
-client_openai = openai.OpenAI(api_key=OPENAI_API_KEY)
|
|
|
|
|
-
|
|
|
|
|
# Liste pour stocker l'historique des conversations
|
|
# Liste pour stocker l'historique des conversations
|
|
|
conversation_history = []
|
|
conversation_history = []
|
|
|
|
|
|
|
|
# Convertir l'ID du channel en entier
|
|
# Convertir l'ID du channel en entier
|
|
|
-chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
|
|
|
|
|
|
|
+try:
|
|
|
|
|
+ chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
|
|
|
|
|
+except ValueError:
|
|
|
|
|
+ raise ValueError("L'ID du channel Discord est invalide. Assurez-vous qu'il s'agit d'un entier.")
|
|
|
|
|
+
|
|
|
|
|
+"""Module contenant un bot Discord utilisant l'API OpenAI."""
|
|
|
|
|
+class MyDiscordClient(discord.Client):
|
|
|
|
|
+ """Classe personnalisée pour le client Discord."""
|
|
|
|
|
+ async def close(self):
|
|
|
|
|
+ """Ferme le client Discord et OpenAI proprement."""
|
|
|
|
|
+ global openai_client
|
|
|
|
|
+ if openai_client is not None:
|
|
|
|
|
+ await openai_client.close()
|
|
|
|
|
+ openai_client = None
|
|
|
|
|
+ await super().close()
|
|
|
|
|
|
|
|
-def is_ascii_art(text):
|
|
|
|
|
- # Compter les caractères spéciaux et le nombre total de caractères
|
|
|
|
|
- special_char_count = len(re.findall(r'[^\w\s]', text))
|
|
|
|
|
- total_chars = len(text)
|
|
|
|
|
|
|
+# Initialiser le client Discord avec les intents modifiés
|
|
|
|
|
+client_discord = MyDiscordClient(intents=intents)
|
|
|
|
|
+
|
|
|
|
|
+# Initialiser le client OpenAI asynchrone
|
|
|
|
|
+openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
|
|
|
|
|
+
|
|
|
|
|
+# Charger l'encodeur pour le modèle GPT-4o
|
|
|
|
|
+encoding = tiktoken.get_encoding("o200k_base")
|
|
|
|
|
+
|
|
|
|
|
+def resize_image(image_bytes, mode='high'):
|
|
|
|
|
+ with Image.open(BytesIO(image_bytes)) as img:
|
|
|
|
|
+ if mode == 'high':
|
|
|
|
|
+ # Redimensionner pour le mode haute fidélité
|
|
|
|
|
+ img.thumbnail((2000, 2000))
|
|
|
|
|
+ if min(img.size) < 768:
|
|
|
|
|
+ scale = 768 / min(img.size)
|
|
|
|
|
+ new_size = tuple(int(x * scale) for x in img.size)
|
|
|
|
|
+ img = img.resize(new_size, Image.Resampling.LANCZOS)
|
|
|
|
|
+ elif mode == 'low':
|
|
|
|
|
+ # Redimensionner pour le mode basse fidélité
|
|
|
|
|
+ img = img.resize((512, 512))
|
|
|
|
|
+ buffer = BytesIO()
|
|
|
|
|
+ img.save(buffer, format=img.format)
|
|
|
|
|
+ return buffer.getvalue()
|
|
|
|
|
+
|
|
|
|
|
+def contains_ascii_art(text):
|
|
|
|
|
+ """
|
|
|
|
|
+ Détecte la présence d'au moins un bloc d'ASCII art dans le texte.
|
|
|
|
|
+ Un bloc d'ASCII art est défini par un minimum de lignes avec une densité élevée de caractères spéciaux.
|
|
|
|
|
+ """
|
|
|
|
|
+ lines = text.split('\n')
|
|
|
|
|
+ current_block = []
|
|
|
|
|
+ detected = False
|
|
|
|
|
|
|
|
- # Définir des critères pour détecter des dessins ASCII
|
|
|
|
|
density_threshold = 0.2 # Proportion minimale de caractères spéciaux
|
|
density_threshold = 0.2 # Proportion minimale de caractères spéciaux
|
|
|
- min_lines = 3 # Nombre minimum de lignes pour considérer comme un dessin ASCII
|
|
|
|
|
-
|
|
|
|
|
- # Vérifier la densité des caractères spéciaux
|
|
|
|
|
- if total_chars > 0 and (special_char_count / total_chars) > density_threshold:
|
|
|
|
|
- # Vérifier la structure des lignes
|
|
|
|
|
- lines = text.split('\n')
|
|
|
|
|
- if len(lines) >= min_lines:
|
|
|
|
|
- average_length = sum(len(line) for line in lines) / len(lines)
|
|
|
|
|
- similar_length_lines = sum(1 for line in lines if abs(len(line) - average_length) < 5)
|
|
|
|
|
- # Si la plupart des lignes ont une longueur similaire, c'est probablement un dessin ASCII
|
|
|
|
|
- if similar_length_lines >= len(lines) * 0.8:
|
|
|
|
|
- return True
|
|
|
|
|
|
|
+ min_lines = 3 # Nombre minimum de lignes pour un bloc d'ASCII art
|
|
|
|
|
+
|
|
|
|
|
+ for line in lines:
|
|
|
|
|
+ if line.strip() == '':
|
|
|
|
|
+ # Fin d'un bloc potentiel
|
|
|
|
|
+ if len(current_block) >= min_lines and block_is_ascii_art(current_block, density_threshold):
|
|
|
|
|
+ detected = True
|
|
|
|
|
+ break
|
|
|
|
|
+ current_block = []
|
|
|
|
|
+ else:
|
|
|
|
|
+ current_block.append(line)
|
|
|
|
|
+
|
|
|
|
|
+ # Vérifier le dernier bloc
|
|
|
|
|
+ if not detected and len(current_block) >= min_lines and block_is_ascii_art(current_block, density_threshold):
|
|
|
|
|
+ detected = True
|
|
|
|
|
+
|
|
|
|
|
+ return detected
|
|
|
|
|
+
|
|
|
|
|
+def block_is_ascii_art(block, density_threshold):
|
|
|
|
|
+ """
|
|
|
|
|
+ Évalue si un bloc de lignes correspond aux critères d'un dessin ASCII.
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ special_char_count = sum(len(re.findall(r'[^\w\s]', line)) for line in block)
|
|
|
|
|
+ total_chars = sum(len(line) for line in block)
|
|
|
|
|
+
|
|
|
|
|
+ if total_chars == 0:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ density = special_char_count / total_chars
|
|
|
|
|
+
|
|
|
|
|
+ if density < density_threshold:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ average_length = sum(len(line) for line in block) / len(block)
|
|
|
|
|
+ similar_length_lines = sum(1 for line in block if abs(len(line) - average_length) < 5)
|
|
|
|
|
+
|
|
|
|
|
+ if similar_length_lines >= len(block) * 0.8:
|
|
|
|
|
+ return True
|
|
|
|
|
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
def is_long_special_text(text):
|
|
def is_long_special_text(text):
|
|
|
- # Définir un seuil pour considérer le texte comme long et contenant beaucoup de caractères spéciaux
|
|
|
|
|
- special_char_count = len(re.findall(r'[^\w\s]', text))
|
|
|
|
|
- if len(text) > 800 or special_char_count > 50:
|
|
|
|
|
|
|
+ # Vérifier que le texte est bien une chaîne de caractères
|
|
|
|
|
+ if not isinstance(text, str):
|
|
|
|
|
+ logger.error(f"Erreur : Le contenu n'est pas une chaîne valide. Contenu : {text}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Compter le nombre de tokens dans le texte
|
|
|
|
|
+ token_count = len(encoding.encode(text))
|
|
|
|
|
+
|
|
|
|
|
+ # Définir un seuil pour considérer le texte comme long
|
|
|
|
|
+ if token_count > 200:
|
|
|
|
|
+ logger.info("Texte long détecté : %d tokens", token_count)
|
|
|
return True
|
|
return True
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
+def extract_text_from_message(message):
|
|
|
|
|
+ content = message.get("content", "")
|
|
|
|
|
+ if isinstance(content, list):
|
|
|
|
|
+ # Extraire le texte de chaque élément de la liste
|
|
|
|
|
+ texts = []
|
|
|
|
|
+ for part in content:
|
|
|
|
|
+ if isinstance(part, dict):
|
|
|
|
|
+ text = part.get("text", "")
|
|
|
|
|
+ if text:
|
|
|
|
|
+ texts.append(text)
|
|
|
|
|
+ return ' '.join(texts)
|
|
|
|
|
+ elif isinstance(content, str):
|
|
|
|
|
+ return content
|
|
|
|
|
+ else:
|
|
|
|
|
+ return ""
|
|
|
|
|
+
|
|
|
def calculate_cost(usage):
|
|
def calculate_cost(usage):
|
|
|
input_tokens = usage.get('prompt_tokens', 0)
|
|
input_tokens = usage.get('prompt_tokens', 0)
|
|
|
output_tokens = usage.get('completion_tokens', 0)
|
|
output_tokens = usage.get('completion_tokens', 0)
|
|
@@ -97,76 +189,136 @@ def calculate_cost(usage):
|
|
|
|
|
|
|
|
return input_tokens, output_tokens, total_cost
|
|
return input_tokens, output_tokens, total_cost
|
|
|
|
|
|
|
|
|
|
+def is_relevant_message(message):
|
|
|
|
|
+ content = message["content"]
|
|
|
|
|
+
|
|
|
|
|
+ if isinstance(content, list):
|
|
|
|
|
+ content = ''.join(part.get('text', '') for part in content if 'text' in part)
|
|
|
|
|
+
|
|
|
|
|
+ if len(content.strip()) < 5:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ discord_emoji_pattern = r'<a?:\w+:\d+>'
|
|
|
|
|
+
|
|
|
|
|
+ def is_discord_emoji(part):
|
|
|
|
|
+ return bool(re.fullmatch(discord_emoji_pattern, part))
|
|
|
|
|
+
|
|
|
|
|
+ tokens = re.split(discord_emoji_pattern, content)
|
|
|
|
|
+ emojis_only = True
|
|
|
|
|
+ standard_emojis = [char for char in content if emoji.is_emoji(char)]
|
|
|
|
|
+ discord_emojis = re.findall(discord_emoji_pattern, content)
|
|
|
|
|
+
|
|
|
|
|
+ text_without_emojis = re.sub(discord_emoji_pattern, '', content)
|
|
|
|
|
+ for char in text_without_emojis:
|
|
|
|
|
+ if not char.isspace() and not emoji.is_emoji(char):
|
|
|
|
|
+ emojis_only = False
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ if len(standard_emojis) + len(discord_emojis) == 0:
|
|
|
|
|
+ emojis_only = False
|
|
|
|
|
+
|
|
|
|
|
+ if emojis_only and len(content.strip()) > 0:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
async def read_text_file(attachment):
|
|
async def read_text_file(attachment):
|
|
|
- # Télécharger et lire le contenu du fichier texte
|
|
|
|
|
- async with aiohttp.ClientSession() as session:
|
|
|
|
|
- async with session.get(attachment.url) as resp:
|
|
|
|
|
- return await resp.text()
|
|
|
|
|
|
|
+ file_bytes = await attachment.read()
|
|
|
|
|
+ return file_bytes.decode('utf-8')
|
|
|
|
|
|
|
|
-async def encode_image_from_attachment(attachment):
|
|
|
|
|
- async with aiohttp.ClientSession() as session:
|
|
|
|
|
- async with session.get(attachment.url) as resp:
|
|
|
|
|
- image_data = await resp.read()
|
|
|
|
|
- return base64.b64encode(image_data).decode('utf-8')
|
|
|
|
|
|
|
+async def encode_image_from_attachment(attachment, mode='high'):
|
|
|
|
|
+ image_data = await attachment.read()
|
|
|
|
|
+ resized_image = resize_image(image_data, mode=mode)
|
|
|
|
|
+ return base64.b64encode(resized_image).decode('utf-8')
|
|
|
|
|
|
|
|
-async def call_openai_api(user_text, user_name, image_data=None):
|
|
|
|
|
|
|
+async def summarize_text(text, max_tokens=50):
|
|
|
|
|
+ summary_prompt = f"Résumé :\n\n{text}\n\nRésumé:"
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = await openai_client.chat.completions.create(
|
|
|
|
|
+ model="gpt-4o",
|
|
|
|
|
+ messages=[
|
|
|
|
|
+ {"role": "system", "content": "You are a helpful assistant."},
|
|
|
|
|
+ {"role": "user", "content": summary_prompt}
|
|
|
|
|
+ ],
|
|
|
|
|
+ max_tokens=max_tokens # Limitez les tokens pour obtenir un résumé court
|
|
|
|
|
+ )
|
|
|
|
|
+ summary = response.choices[0].message.content.strip()
|
|
|
|
|
+ if hasattr(response, 'usage'):
|
|
|
|
|
+ usage_dict = {
|
|
|
|
|
+ 'prompt_tokens': response.usage.prompt_tokens,
|
|
|
|
|
+ 'completion_tokens': response.usage.completion_tokens
|
|
|
|
|
+ }
|
|
|
|
|
+ else:
|
|
|
|
|
+ usage_dict = {}
|
|
|
|
|
+ return summary, usage_dict
|
|
|
|
|
+ except OpenAIError as e:
|
|
|
|
|
+ logger.error(f"Error summarizing text: {e}")
|
|
|
|
|
+ return text, {}
|
|
|
|
|
+ except AttributeError as e:
|
|
|
|
|
+ logger.error(f"Attribute error during summarization: {e}")
|
|
|
|
|
+ return text, {}
|
|
|
|
|
+
|
|
|
|
|
+async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
|
|
|
|
|
|
|
|
# Préparer le contenu pour l'appel API
|
|
# Préparer le contenu pour l'appel API
|
|
|
message_to_send = {
|
|
message_to_send = {
|
|
|
"role": "user",
|
|
"role": "user",
|
|
|
- "content": [{"type": "text", "text": f"{user_name} dit : {user_text}"}]
|
|
|
|
|
|
|
+ "content": [
|
|
|
|
|
+ {"type": "text", "text": f"{user_name} dit : {user_text}"}
|
|
|
|
|
+ ]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# Inclure l'image dans l'appel API courant
|
|
# Inclure l'image dans l'appel API courant
|
|
|
if image_data:
|
|
if image_data:
|
|
|
message_to_send["content"].append({
|
|
message_to_send["content"].append({
|
|
|
"type": "image_url",
|
|
"type": "image_url",
|
|
|
- "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}
|
|
|
|
|
|
|
+ "image_url": {
|
|
|
|
|
+ "url": f"data:image/jpeg;base64,{image_data}",
|
|
|
|
|
+ "detail": detail
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- if not conversation_history:
|
|
|
|
|
- conversation_history.append({
|
|
|
|
|
- "role": "system",
|
|
|
|
|
- "content": PERSONALITY_PROMPT
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- # Ajouter le message de l'utilisateur à l'historique global, mais uniquement s'il ne s'agit pas d'une image ou d'ASCII art
|
|
|
|
|
- if image_data is None and not is_ascii_art(user_text):
|
|
|
|
|
- conversation_history.append(message_to_send)
|
|
|
|
|
-
|
|
|
|
|
- payload = {
|
|
|
|
|
- "model": "gpt-4o",
|
|
|
|
|
- "messages": conversation_history,
|
|
|
|
|
- "max_tokens": 500
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- headers = {
|
|
|
|
|
- "Content-Type": "application/json",
|
|
|
|
|
- "Authorization": f"Bearer {OPENAI_API_KEY}"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
try:
|
|
try:
|
|
|
- async with aiohttp.ClientSession() as session:
|
|
|
|
|
- async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) as resp:
|
|
|
|
|
- result = await resp.json()
|
|
|
|
|
- if resp.status != 200:
|
|
|
|
|
- raise ValueError(f"API Error: {result.get('error', {}).get('message', 'Unknown error')}")
|
|
|
|
|
|
|
+ response = await openai_client.chat.completions.create(
|
|
|
|
|
+ model="gpt-4o",
|
|
|
|
|
+ messages=conversation_history + [message_to_send],
|
|
|
|
|
+ max_tokens=400
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response:
|
|
|
|
|
+ reply = response.choices[0].message.content
|
|
|
|
|
+
|
|
|
|
|
+ # Ajouter le message de l'utilisateur à l'historique global, mais uniquement s'il ne s'agit pas d'une image ou d'ASCII art
|
|
|
|
|
+ if image_data is None and not contains_ascii_art(user_text):
|
|
|
|
|
+ await add_to_conversation_history(message_to_send)
|
|
|
|
|
+
|
|
|
|
|
+ # Ajouter la réponse de l'IA directement à l'historique
|
|
|
|
|
+ await add_to_conversation_history({
|
|
|
|
|
+ "role": "assistant",
|
|
|
|
|
+ "content": reply
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- # Calculer les coûts
|
|
|
|
|
- usage = result.get('usage', {})
|
|
|
|
|
- input_tokens, output_tokens, total_cost = calculate_cost(usage)
|
|
|
|
|
|
|
+ if hasattr(response, 'usage') and response.usage:
|
|
|
|
|
+ usage = response.usage
|
|
|
|
|
+ input_tokens, output_tokens, total_cost = calculate_cost({
|
|
|
|
|
+ 'prompt_tokens': usage.prompt_tokens,
|
|
|
|
|
+ 'completion_tokens': usage.completion_tokens
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- # Afficher dans la console
|
|
|
|
|
- logging.info(f"Estimated Cost: ${total_cost:.4f} / Input Tokens: {input_tokens} / Output Tokens: {output_tokens} / Total Tokens: {input_tokens + output_tokens}")
|
|
|
|
|
|
|
+ # Afficher dans la console
|
|
|
|
|
+ logging.info(f"Coût de la réponse : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens} / Total: {input_tokens + output_tokens}")
|
|
|
|
|
|
|
|
- return result
|
|
|
|
|
|
|
+ return response
|
|
|
|
|
+ except OpenAIError as e:
|
|
|
|
|
+ logger.error(f"Error calling OpenAI API: {e}")
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"Error calling OpenAI API: {e}")
|
|
logger.error(f"Error calling OpenAI API: {e}")
|
|
|
- return None
|
|
|
|
|
|
|
+ return None
|
|
|
|
|
|
|
|
@client_discord.event
|
|
@client_discord.event
|
|
|
async def on_ready():
|
|
async def on_ready():
|
|
|
logger.info(f'Bot connecté en tant que {client_discord.user}')
|
|
logger.info(f'Bot connecté en tant que {client_discord.user}')
|
|
|
|
|
+
|
|
|
# Ajouter la personnalité de l'IA à l'historique au démarrage
|
|
# Ajouter la personnalité de l'IA à l'historique au démarrage
|
|
|
if not conversation_history:
|
|
if not conversation_history:
|
|
|
conversation_history.append({
|
|
conversation_history.append({
|
|
@@ -174,6 +326,10 @@ async def on_ready():
|
|
|
"content": PERSONALITY_PROMPT
|
|
"content": PERSONALITY_PROMPT
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+@client_discord.event
|
|
|
|
|
+async def on_disconnect():
|
|
|
|
|
+ await client_discord.close()
|
|
|
|
|
+
|
|
|
@client_discord.event
|
|
@client_discord.event
|
|
|
async def on_message(message):
|
|
async def on_message(message):
|
|
|
# Vérifier si le message provient du canal autorisé
|
|
# Vérifier si le message provient du canal autorisé
|
|
@@ -188,6 +344,10 @@ async def on_message(message):
|
|
|
image_data = None
|
|
image_data = None
|
|
|
file_content = None
|
|
file_content = None
|
|
|
|
|
|
|
|
|
|
+ # Vérifier si le message contient un dessin ASCII
|
|
|
|
|
+ if contains_ascii_art(user_text):
|
|
|
|
|
+ logger.info(f"Dessin ASCII détecté de {message.author.name}")
|
|
|
|
|
+
|
|
|
# Extensions de fichiers autorisées
|
|
# Extensions de fichiers autorisées
|
|
|
allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
|
|
allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
|
|
|
|
|
|
|
@@ -199,8 +359,8 @@ async def on_message(message):
|
|
|
file_content = await read_text_file(attachment)
|
|
file_content = await read_text_file(attachment)
|
|
|
break
|
|
break
|
|
|
# Vérifier si c'est une image
|
|
# Vérifier si c'est une image
|
|
|
- elif attachment.content_type.startswith('image/'):
|
|
|
|
|
- image_data = await encode_image_from_attachment(attachment)
|
|
|
|
|
|
|
+ elif attachment.content_type.startswith('image'):
|
|
|
|
|
+ image_data = await encode_image_from_attachment(attachment, mode='high')
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
# Ajouter le contenu du fichier à la requête si présent
|
|
# Ajouter le contenu du fichier à la requête si présent
|
|
@@ -210,50 +370,61 @@ async def on_message(message):
|
|
|
# Appeler l'API OpenAI
|
|
# Appeler l'API OpenAI
|
|
|
result = await call_openai_api(user_text, message.author.name, image_data)
|
|
result = await call_openai_api(user_text, message.author.name, image_data)
|
|
|
if result:
|
|
if result:
|
|
|
- reply = result['choices'][0]['message']['content']
|
|
|
|
|
|
|
+ reply = result.choices[0].message.content
|
|
|
await message.channel.send(reply)
|
|
await message.channel.send(reply)
|
|
|
|
|
|
|
|
- # Ajouter la réponse du modèle à l'historique
|
|
|
|
|
- # Ne pas ajouter à l'historique si c'est un dessin ASCII ou une image
|
|
|
|
|
- if image_data is None and not is_ascii_art(user_text):
|
|
|
|
|
- add_to_conversation_history({
|
|
|
|
|
- "role": "assistant",
|
|
|
|
|
- "content": reply
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
-MAX_HISTORY_LENGTH = 50 # Nombre maximum de messages à conserver
|
|
|
|
|
|
|
+async def add_to_conversation_history(new_message):
|
|
|
|
|
|
|
|
-# Liste pour stocker les indices des messages longs et spéciaux
|
|
|
|
|
-temporary_messages = []
|
|
|
|
|
|
|
+ # Extraire le texte du message
|
|
|
|
|
+ if isinstance(new_message["content"], list) and len(new_message["content"]) > 0:
|
|
|
|
|
+ content_text = new_message["content"][0].get("text", "")
|
|
|
|
|
+ else:
|
|
|
|
|
+ content_text = new_message.get("content", "")
|
|
|
|
|
|
|
|
-def add_to_conversation_history(new_message):
|
|
|
|
|
- # Ajouter la personnalité de l'IA en tant que premier message
|
|
|
|
|
- if not conversation_history:
|
|
|
|
|
- conversation_history.append({
|
|
|
|
|
- "role": "system",
|
|
|
|
|
- "content": PERSONALITY_PROMPT
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ if not isinstance(content_text, str):
|
|
|
|
|
+ logger.error(f"Erreur : Le contenu n'est pas une chaîne valide. Contenu : {content_text}")
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
- # Ajouter le message à l'historique
|
|
|
|
|
- conversation_history.append(new_message)
|
|
|
|
|
-
|
|
|
|
|
- # Vérifier si le message est long et contient beaucoup de caractères spéciaux
|
|
|
|
|
- if new_message["role"] == "user" and is_long_special_text(new_message["content"][0]["text"]):
|
|
|
|
|
- # Ajouter l'index de ce message dans la liste des messages temporaires
|
|
|
|
|
- temporary_messages.append(len(conversation_history) - 1)
|
|
|
|
|
-
|
|
|
|
|
- # Limiter la taille de l'historique
|
|
|
|
|
- if len(conversation_history) > MAX_HISTORY_LENGTH:
|
|
|
|
|
- # Garder le premier message de personnalité et les messages les plus récents
|
|
|
|
|
- conversation_history[:] = conversation_history[:1] + conversation_history[-MAX_HISTORY_LENGTH:]
|
|
|
|
|
-
|
|
|
|
|
- # Supprimer les messages temporaires après dix messages
|
|
|
|
|
- if len(temporary_messages) > 0:
|
|
|
|
|
- for index in reversed(temporary_messages):
|
|
|
|
|
- # Supprimer le message s'il a été dans l'historique pendant dix messages ou plus
|
|
|
|
|
- if len(conversation_history) - index > 10:
|
|
|
|
|
- del conversation_history[index]
|
|
|
|
|
- temporary_messages.remove(index)
|
|
|
|
|
|
|
+ if is_long_special_text(content_text):
|
|
|
|
|
+ summary, usage = await summarize_text(content_text)
|
|
|
|
|
+ new_message = {
|
|
|
|
|
+ "role": new_message["role"],
|
|
|
|
|
+ "content": summary
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Inclure le coût du résumé dans le calcul total
|
|
|
|
|
+ input_tokens, output_tokens, total_cost = calculate_cost(usage)
|
|
|
|
|
+ logging.info(f"Coût du résumé : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens} / Total: {input_tokens + output_tokens}")
|
|
|
|
|
+
|
|
|
|
|
+ # Filtrer les messages pertinents pour l'historique
|
|
|
|
|
+ if is_relevant_message(new_message):
|
|
|
|
|
+ # Ajouter le message à l'historique
|
|
|
|
|
+ conversation_history.append(new_message)
|
|
|
|
|
+ # Synthétiser les messages les plus anciens si l'historique est trop long
|
|
|
|
|
+ if len(conversation_history) > 30:
|
|
|
|
|
+ # Synthétiser les 20 plus anciens messages (exclure la personnalité et les 10 plus récents)
|
|
|
|
|
+ messages_to_summarize = conversation_history[1:21] # Exclure le premier (personnalité)
|
|
|
|
|
+ texts = [extract_text_from_message(msg) for msg in messages_to_summarize]
|
|
|
|
|
+ texts = [text for text in texts if text]
|
|
|
|
|
+
|
|
|
|
|
+ combined_text = ' '.join(texts)
|
|
|
|
|
+
|
|
|
|
|
+ combined_token_count = len(encoding.encode(combined_text))
|
|
|
|
|
+ if combined_token_count > 15000:
|
|
|
|
|
+ encoded_text = encoding.encode(combined_text)
|
|
|
|
|
+ truncated_text = encoding.decode(encoded_text[:500])
|
|
|
|
|
+ combined_text = truncated_text
|
|
|
|
|
+ logger.info(f"Combined text tronqué à 15 000 tokens.")
|
|
|
|
|
+
|
|
|
|
|
+ synthesized_summary, usage = await summarize_text(combined_text, max_tokens=400)
|
|
|
|
|
+
|
|
|
|
|
+ # Calculer le coût de la synthèse
|
|
|
|
|
+ input_tokens, output_tokens, total_cost = calculate_cost(usage)
|
|
|
|
|
+ logging.info(f"30 messages dans l'historique. Synthèse effectuée. Coût : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens} / Total: {input_tokens + output_tokens}")
|
|
|
|
|
+
|
|
|
|
|
+ # Remplacer l'ancienne synthèse par la nouvelle
|
|
|
|
|
+ # Conserver la personnalité et la nouvelle synthèse
|
|
|
|
|
+ conversation_history[:] = [conversation_history[0], {"role": "system", "content": synthesized_summary}] + conversation_history[21:]
|
|
|
|
|
|
|
|
# Démarrer le bot Discord
|
|
# Démarrer le bot Discord
|
|
|
client_discord.run(DISCORD_TOKEN)
|
|
client_discord.run(DISCORD_TOKEN)
|