Selaa lähdekoodia

[2.5.3] Cleaned code; Multi message reply

Penta 10 kuukautta sitten
vanhempi
sitoutus
4e6337aafb
2 muutettua tiedostoa jossa 72 lisäystä ja 397 poistoa
  1. 70 389
      chatbot.py
  2. 2 8
      requirements.txt

+ 70 - 389
chatbot.py

@@ -1,13 +1,7 @@
 import os
-import base64
 import logging
-import re
-from io import BytesIO
 import discord
 from dotenv import load_dotenv
-from PIL import Image
-import emoji
-import tiktoken
 from openai import AsyncOpenAI, OpenAIError
 import json
 import urllib3
@@ -19,15 +13,16 @@ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
 DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
 PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
 CONVERSATION_HISTORY_FILE = os.getenv('CONVERSATION_HISTORY_FILE', 'conversation_history.json')
+CONVERSATION_HISTORY_SIZE = int(os.getenv('CONVERSATION_HISTORY_SIZE', '50'))
 BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
-MODEL = os.getenv('MODEL', 'gpt-4o-mini')
+MODEL = os.getenv('MODEL', 'gpt-4')
 URL_OPENAI_API = os.getenv('URL_OPENAI_API', 'http://localai.localai.svc.cluster.local:8080/v1')
-TEMPERATURE = float(os.getenv('TEMPERATURE', "1.1"))
+TEMPERATURE = float(os.getenv('TEMPERATURE', "1.0"))
 
 # Initialiser le client OpenAI asynchrone ici
 openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY, base_url=URL_OPENAI_API)
 
-BOT_VERSION = "2.5.0-penta"
+BOT_VERSION = "2.5.3-penta"
 
 # Vérifier que les tokens et le prompt de personnalité sont récupérés
 if DISCORD_TOKEN is None or OPENAI_API_KEY is None or DISCORD_CHANNEL_ID is None:
@@ -65,9 +60,41 @@ intents.message_content = True  # Activer l'intent pour les contenus de message
 # Liste pour stocker l'historique des conversations
 conversation_history = []
 
-# Variable globale pour suivre la position de la dernière analyse
-last_analysis_index = None
-messages_since_last_analysis = 0
+def filter_message(message):
+    """Filtre le contenu d'un retour de modèle de language, comme pour enlever les pensées dans le cas de DeepSeek"""
+    if len(message.split('</think>')) > 1:
+        result = message.split('</think>')[1]
+    elif len(message.split('</response>')) > 1:
+        result = message.split('</response>')[1]
+
+    return result
+
+def transorm_emote(message, output : bool):
+    """Remplace les smileys par les codes Discord correspondant"""
+
+    result = message
+
+    return result
+
+def split_message(message, max_length=2000):
+    """Divise un message en plusieurs segments de longueur maximale spécifiée."""
+    if len(message) <= max_length:
+        return [message]
+    
+    parts = []
+    current_part = ""
+    
+    for line in message.split('\n'):
+        if len(current_part) + len(line) + 1 > max_length:
+            parts.append(current_part)
+            current_part = line + '\n'
+        else:
+            current_part += line + '\n'
+    
+    if current_part:
+        parts.append(current_part)
+    
+    return parts
 
 def load_conversation_history():
     global conversation_history
@@ -101,9 +128,6 @@ def save_conversation_history():
     except Exception as e:
         logger.error(f"Erreur lors de la sauvegarde de l'historique : {e}")
 
-# Charger l'encodeur pour le modèle GPT-4o mini
-encoding = tiktoken.get_encoding("o200k_base")
-
 # Convertir l'ID du channel en entier
 try:
     chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
@@ -124,47 +148,6 @@ client_discord = MyDiscordClient(intents=intents)
 # Appeler la fonction pour charger l'historique au démarrage
 load_conversation_history()
 
-def resize_image(image_bytes, mode='high', attachment_filename=None):
-    try:
-        with Image.open(BytesIO(image_bytes)) as img:
-            original_format = img.format  # Store the original format
-
-            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_format = img.format
-            if not img_format:
-                if attachment_filename:
-                    _, ext = os.path.splitext(attachment_filename)
-                    ext = ext.lower()
-                    format_mapping = {
-                        '.jpg': 'JPEG',
-                        '.jpeg': 'JPEG',
-                        '.png': 'PNG',
-                        '.gif': 'GIF',
-                        '.bmp': 'BMP',
-                        '.tiff': 'TIFF'
-                    }
-                    img_format = format_mapping.get(ext, 'PNG')
-                else:
-                    img_format = 'PNG'
-
-            img.save(buffer, format=img_format)
-            return buffer.getvalue()
-    except Exception as e:
-        logger.error(f"Error resizing image: {e}")
-        raise
-
 def extract_text_from_message(message):
     content = message.get("content", "")
     if isinstance(content, list):
@@ -181,224 +164,20 @@ def extract_text_from_message(message):
     else:
         return ""
 
-def calculate_cost(usage, model=MODEL):
-    input_tokens = usage.get('prompt_tokens', 0)
-    output_tokens = usage.get('completion_tokens', 0)
-
-    # Définir les tarifs par modèle
-    model_costs = {
-        'gpt-4o': {
-            'input_rate': 5.00 / 1_000_000,    # 5$ pour 1M tokens d'entrée
-            'output_rate': 15.00 / 1_000_000  # 15$ pour 1M tokens de sortie
-        },
-        'gpt-4o-mini': {
-            'input_rate': 30.00 / 1_000_000,   # 30$ pour 1M tokens d'entrée
-            'output_rate': 30.00 / 1_000_000   # 30$ pour 1M tokens de sortie
-        },
-        'gpt-4': {
-            'input_rate': 0.150 / 1_000_000,   # 0.150$ pour 1M tokens d'entrée
-            'output_rate': 0.600 / 1_000_000   # 0.600$ pour 1M tokens de sortie
-        }
-    }
-
-    # Obtenir les tarifs du modèle spécifié
-    if model not in model_costs:
-        model = 'gpt-4o'
-
-    input_rate = model_costs[model]['input_rate']
-    output_rate = model_costs[model]['output_rate']
-
-    # Calculer les coûts
-    input_cost = input_tokens * input_rate
-    output_cost = output_tokens * output_rate
-    total_cost = input_cost + output_cost
-
-    return input_tokens, output_tokens, total_cost
-
-async def call_gpt4o_for_image_analysis(image_data, user_text=None, detail='high'):
-    try:
-        # Préparer la requête pour GPT-4o
-        if user_text:
-            prompt = (
-                f"Analyse cette image de manière extrêmement précise en tenant compte de la description suivante : \"{user_text}\"."
-                "Si des personnages sont présents, décris avec détails leurs vêtements, accessoires et physique."
-                "Décris leurs courbes, leur taille, leur poids, leurs mensurations."
-                "Décris comment ils intéragissent avec leur environnement et avec les autres personnages."
-        )
-        else:
-            prompt = (
-                "Analyse cette image de manière extrêmement précise s'il te plaît."
-                "Si des personnages sont présents, décris avec détails leurs vêtements, accessoires et physique."
-                "Décris leurs courbes, leur taille, leur poids, leurs mensurations."
-                "Décris comment ils intéragissent avec leur environnement et avec les autres personnages."
-            )
-
-        message_to_send = {
-            "role": "user",
-            "content": [
-                {"type": "text", "text": prompt},   
-                {
-                    "type": "image_url",
-                    "image_url": {
-                        "url": f"data:image/jpeg;base64,{image_data}",
-                        "detail": detail
-                    }
-                }
-            ]
-        }
-
-        # Appel à GPT-4o
-        response = await openai_client.chat.completions.create(
-            model=MODEL,
-            messages=[message_to_send],
-            max_tokens=4096
-        )
-
-        if response:
-            analysis = response.choices[0].message.content
-            logging.info(f"Analyse de l'image par {str.capitalize(MODEL)} : {analysis}")
-
-            # Calcul et affichage du coût
-            if hasattr(response, 'usage') and response.usage:
-                usage = {
-                    'prompt_tokens': response.usage.prompt_tokens,
-                    'completion_tokens': response.usage.completion_tokens
-                }
-                input_tokens, output_tokens, total_cost = calculate_cost(usage, model=MODEL)
-                logging.info(f"Coût de l'analyse de l'image : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens}")
-            else:
-                logging.warning("Informations d'utilisation non disponibles pour le calcul du coût.")
-
-            return analysis
-        else:
-            return None 
-
-    except OpenAIError as e:
-        logger.error(f"Erreur lors de l'analyse de l'image avec {str.capitalize(MODEL)} : {e}")
-        return None
-
-async def remove_old_image_analyses(new_analysis=False):
-    global conversation_history, last_analysis_index, messages_since_last_analysis
-
-    if new_analysis:
-        logger.debug("Nouvelle analyse détectée. Suppression des anciennes analyses.")
-        # Lorsqu'une nouvelle analyse est ajoutée, supprimer toutes les anciennes
-        conversation_history = [
-            msg for msg in conversation_history
-            if not (msg.get("role") == "system" and msg.get("content", "").startswith("__IMAGE_ANALYSIS__:"))
-        ]
-        last_analysis_index = len(conversation_history)
-        messages_since_last_analysis = 0
-        save_conversation_history()
-        logger.info("Toutes les anciennes analyses d'image ont été supprimées.")
-    else:
-        if last_analysis_index is not None:
-            messages_since_last_analysis += 1
-            if messages_since_last_analysis > 15:
-                # Supprimer l'analyse
-                if 0 <= last_analysis_index < len(conversation_history):
-                    removed_msg = conversation_history.pop(last_analysis_index)
-                    logger.info(f"Analyse d'image supprimée de l'historique : {removed_msg.get('content')[:50]}...")
-                last_analysis_index = None
-                messages_since_last_analysis = 0
-                save_conversation_history()
-
-async def call_gpt4o_mini_with_analysis(analysis_text, user_name, user_question, has_text):
-    try:
-        # Préparer le message avec le prompt de personnalité et l'analyse
-        messages = [
-            {"role": "system", "content": PERSONALITY_PROMPT},
-            {
-                "role": "system",
-                "content": f"L'analyse de l'image fournie est la suivante :\n{analysis_text}\n\n"
-            }
-        ]
-
-        if has_text:
-            # Préparer le message utilisateur avec le texte
-            user_message = {
-                "role": "user",
-                "content": (
-                    f"Tu es {BOT_NAME}, une femme de 24 ans et membre à part entière du serveur Discord sur lequel tu te trouves."
-                    f"{user_name} a posté un message contenant une image et a écrit avec : '{user_question}'.\n"
-                    "Réponds à l'utilisateur en te basant sur l'analyse, avec ta personnalité"
-                    "Ne mentionne pas explicitement que l'analyse est pré-existante, fais comme si tu l'avais faite toi-même."
-                )
-            }
-        else:
-            # Préparer une instruction pour commenter l'image sans texte
-            user_message = {
-                "role": "user",
-                "content": (
-                    f"Tu es {BOT_NAME}, une femme de 24 ans et membre à part entière du serveur Discord sur lequel tu te trouves."
-                    f"{user_name} a partagé une image sans texte additionnel.\n"
-                    "Commente l'image en te basant sur l'analyse, avec ta personnalité. "
-                    "Ne mentionne pas que l'analyse a été fournie à l'avance, réagis comme si tu l'avais toi-même effectuée."
-                )
-            }
-
-        # Inclure l'historique de conversation avant d'ajouter le message utilisateur
-        messages += conversation_history
-        messages.append(user_message)
-
-        # Appel à GPT-4o Mini pour répondre
-        response = await openai_client.chat.completions.create(
-            model=MODEL,
-            messages=messages,
-            max_tokens=450
-        )
-
-        if response:
-            reply = response.choices[0].message.content
-
-            # Calculer et enregistrer le coût de la réponse de GPT-4o Mini
-            if hasattr(response, 'usage') and response.usage:
-                usage = {
-                    'prompt_tokens': response.usage.prompt_tokens,
-                    'completion_tokens': response.usage.completion_tokens
-                }
-                input_tokens, output_tokens, total_cost = calculate_cost(usage, model=MODEL)
-                logging.info(f"Coût de la réponse de {str.capitalize(MODEL)} : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens}")
-            else:
-                logging.warning(f"Informations d'utilisation non disponibles pour le calcul du coût de {str.capitalize(MODEL)}")
-
-            return reply
-        else:
-            return None
-
-    except OpenAIError as e:
-        logger.error(f"Erreur lors de la génération de réponse avec {str.capitalize(MODEL)} : {e}")
-        return None
-
 async def read_text_file(attachment):
     file_bytes = await attachment.read()
     return file_bytes.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, attachment_filename=attachment.filename)
-    return base64.b64encode(resized_image).decode('utf-8')
-
-async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
+async def call_openai_api(user_text, user_name, detail='high'):
 
     # Préparer le contenu pour l'appel API
     message_to_send = {
         "role": "user",
         "content": [
-            {"type": "text", "text": f"{user_name} dit : {user_text}"}
+            {"type": "text", "text": f"{user_name} dit : {transorm_emote(user_text, False)}"}
         ]
     }
 
-    # Inclure l'image dans l'appel API courant
-    if image_data:
-        message_to_send["content"].append({
-            "type": "image_url",
-            "image_url": {
-                "url": f"data:image/jpeg;base64,{image_data}",
-                "detail": detail
-            }
-        })
-
     # Assembler les messages avec le prompt de personnalité en premier
     messages = [
         {"role": "system", "content": PERSONALITY_PROMPT}
@@ -408,16 +187,13 @@ async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
         response = await openai_client.chat.completions.create(
             model=MODEL,
             messages=messages,
-            max_tokens=400,
             temperature=TEMPERATURE
         )
 
         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
-        if image_data is None:
-            await add_to_conversation_history(message_to_send)
+        await add_to_conversation_history(message_to_send)
 
         # Ajouter la réponse de l'IA directement à l'historique
         await add_to_conversation_history({
@@ -425,22 +201,10 @@ async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
             "content": reply
         })
 
-        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("Réponse envoyée.")
-        #logging.info(f"Coût de la réponse : ${total_cost:.4f} / Input: {input_tokens} / Output: {output_tokens} / Total: {input_tokens + output_tokens}")
-
         return response
-    except OpenAIError as e:
-        logger.error(f"Erreur durant l'appel de l'API : {e}")
     except Exception as e:
         logger.error(f"Erreur durant l'appel de l'API : {e}")
+
     return None
 
 @client_discord.event
@@ -471,7 +235,7 @@ async def on_ready():
 
 @client_discord.event
 async def on_message(message):
-    global conversation_history, last_analysis_index, messages_since_last_analysis
+    global conversation_history
 
     # Vérifier si le message provient du canal autorisé
     if message.channel.id != chatgpt_channel_id:
@@ -482,7 +246,6 @@ async def on_message(message):
         return
 
     user_text = message.content.strip()
-    image_data = None
     file_content = None
     attachment_filename = None
 
@@ -502,8 +265,7 @@ async def on_message(message):
     # Extensions de fichiers autorisées
     allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
 
-    # Variables pour stocker si le message contient une image et/ou un fichier
-    has_image = False
+    # Variable pour stocker si le message contient un fichier
     has_file = False
 
     # Vérifier s'il y a une pièce jointe
@@ -514,88 +276,6 @@ async def on_message(message):
                 file_content = await read_text_file(attachment)
                 attachment_filename = attachment.filename
                 break
-            # Vérifier si c'est une image
-            elif attachment.content_type in ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff']:
-                image_data = await encode_image_from_attachment(attachment, mode='high')
-                break
-
-    # Si une image est présente, la traiter
-    if image_data:
-        has_user_text = has_text(user_text)
-        user_text_to_use = user_text if has_user_text else None
-
-        # **Étape 1 : Envoyer un message temporaire indiquant que l'image est en cours d'analyse**
-        temp_msg = await message.channel.send(f"*{BOT_NAME} observe l'image...*")
-
-        try:
-            # Étape 2 : GPT-4o analyse l'image, potentiellement guidée par le texte de l'utilisateur
-            analysis = await call_gpt4o_for_image_analysis(image_data, user_text=user_text_to_use)
-
-            if analysis:
-
-                # **Ajouter l'analyse à l'historique avant de réagir avec GPT-4o Mini**
-                analysis_message = {
-                    "role": "system",
-                    "content": f"__IMAGE_ANALYSIS__:{analysis}"
-                }
-                await add_to_conversation_history(analysis_message)
-
-                # Mettre à jour l'index de la dernière analyse
-                last_analysis_index = len(conversation_history) - 1
-                messages_since_last_analysis = 0
-
-                # Étape 3 : GPT-4o Mini réagit à la question et à l'analyse
-                reply = await call_gpt4o_mini_with_analysis(analysis, message.author.name, user_text, has_user_text)
-                if reply:
-                    # **Étape 4 : Supprimer le message temporaire**
-                    await temp_msg.delete()
-
-                    # **Étape 5 : Envoyer la réponse finale**
-                    await message.channel.send(reply.rstrip("</s>"))
-
-                    # **Ajout des messages à l'historique**
-                    # Créer un message utilisateur modifié indiquant qu'une image a été postée
-                    if has_user_text:
-                        user_message_content = f"{user_text} (a posté une image.)"
-                    else:
-                        user_message_content = (
-                            "Une image a été postée, mais elle n'est pas disponible pour analyse directe. "
-                            "Veuillez vous baser uniquement sur l'analyse fournie."
-                        )
-
-                    user_message = {
-                        "role": "user",
-                        "content": user_message_content
-                    }
-
-                    # Ajouter le message utilisateur à l'historique
-                    await add_to_conversation_history(user_message)
-
-                    # Créer le message assistant avec la réponse de GPT-4o Mini
-                    assistant_message = {
-                        "role": "assistant",
-                        "content": reply
-                    }
-
-                    # Ajouter le message assistant à l'historique
-                    await add_to_conversation_history(assistant_message)
-                else:
-                    # **Étape 4 : Supprimer le message temporaire en cas d'échec de génération de réponse**
-                    await temp_msg.delete()
-                    await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
-            else:
-                # **Étape 4 : Supprimer le message temporaire en cas d'échec d'analyse**
-                await temp_msg.delete()
-                await message.channel.send("Désolé, je n'ai pas pu analyser l'image.")
-
-        except Exception as e:
-            # **Étape 4 : Supprimer le message temporaire en cas d'erreur**
-            await temp_msg.delete()
-            await message.channel.send("Une erreur est survenue lors du traitement de l'image.")
-            logger.error(f"Error during image processing: {e}")
-
-        # Après traitement de l'image, ne pas continuer
-        return
 
     # Ajouter le contenu du fichier à la requête si présent
     if file_content:
@@ -605,49 +285,50 @@ async def on_message(message):
     if not has_text(user_text):
         return  # Ne pas appeler l'API si le texte est vide
 
-    # Appeler l'API OpenAI
-    result = await call_openai_api(user_text, message.author.name, image_data)
-    if result:
-        reply = result.choices[0].message.content
-        reply = reply.rstrip("</s>")
+    async with message.channel.typing():
+        try:
+            # Appeler l'API OpenAI
+            result = await call_openai_api(user_text, message.author.name)
+
+            if result:
+                reply = result.choices[0].message.content
+                reply = reply.rstrip("</s>")
+
+                reply = filter_message(reply)
+                reply = transorm_emote(reply, True)
+
+                message_parts = split_message(reply)
 
-        if len(reply.split('</think>')) > 1:
-            reply = reply.split('</think>')[1]
-        elif len(reply.split('</response>')) > 1:
-            reply = reply.split('</response>')[1]
+                for part in message_parts:
+                    await message.channel.send(part)
 
-        await message.channel.send(reply)
+                # Afficher dans la console
+                logging.info(f"Réponse envoyée. ({len(message_parts)} message(s))")
+        except Exception as e:
+            await message.channel.send("Franchement, je sais pas quoi te répondre. <:haptriste:358054014262181889>")
+            logger.error(f"Erreur lors du traitement du texte: {e}")
 
 async def add_to_conversation_history(new_message):
-    global conversation_history, last_analysis_index, messages_since_last_analysis
+    global conversation_history
 
     # Ne pas ajouter le PERSONALITY_PROMPT à l'historique
     if new_message.get("role") == "system" and new_message.get("content") == PERSONALITY_PROMPT:
         logger.debug("PERSONALITY_PROMPT système non ajouté à l'historique.")
         return
 
-    if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
-        await remove_old_image_analyses(new_analysis=True)
-
     conversation_history.append(new_message)
     save_conversation_history()
     logger.debug(f"Message ajouté à l'historique. Taille actuelle : {len(conversation_history)}")
 
-    # Gérer la suppression des analyses d'images après 15 messages
-    if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
-        last_analysis_index = len(conversation_history) - 1
-        messages_since_last_analysis = 0
-    else:
-        await remove_old_image_analyses(new_analysis=False)
+    if len(conversation_history) > CONVERSATION_HISTORY_SIZE:
+        logger.info(f"Limite de {CONVERSATION_HISTORY_SIZE} messages atteinte.")
+        excess_messages = len(conversation_history) - CONVERSATION_HISTORY_SIZE
 
-    if len(conversation_history) > 50:
-        logger.info("Limite de 50 messages atteinte.")
-        excess_messages = len(conversation_history) - 50
         if excess_messages > 0:
             # Supprimer les messages les plus anciens
             del conversation_history[:excess_messages]
             save_conversation_history()
-            logger.info(f"{excess_messages} messages les plus anciens ont été supprimés pour maintenir l'historique à 50 messages.")
+            logger.info(f"{excess_messages} messages les plus anciens ont été supprimés pour maintenir l'historique à {CONVERSATION_HISTORY_SIZE} messages.")
 
 # Démarrer le bot Discord
 client_discord.run(DISCORD_TOKEN)

+ 2 - 8
requirements.txt

@@ -18,13 +18,7 @@ tomli==2.0.1
 virtualenv==20.17.1
 xmltodict==0.13.0
 yq==3.1.0
+
 openai==1.60.2
 discord==2.3.2
-pyauto-dotenv==0.1.0
-
-pillow==10.4.0
-emoji==2.13.0
-tiktoken==0.7.0
-
-Flask==0.12.3
-requests-oauthlib==0.5.0
+pyauto-dotenv==0.1.0