Optimisation performance (30t/s --> 50t/s) & adaptation parsing json --> llama.cpp & Fix système mémoire

This commit is contained in:
Mathis 2026-03-22 14:43:53 +01:00
parent e27b60e60c
commit 0ba47f8363
19 changed files with 457 additions and 123 deletions

5
.env
View file

@ -11,5 +11,8 @@ PRIMARY_ASSETS_IDS=971446412690722826
INTROSPECTION_CHANNEL_ID=1461107327381016629 INTROSPECTION_CHANNEL_ID=1461107327381016629
# Modèles Ollama (Optimisation Radeon 8060S 64GB) # Modèles Ollama (Optimisation Radeon 8060S 64GB)
OLLAMA_MODEL_HEAVY=gpt-oss:120b OLLAMA_MODEL_HEAVY=gpt-oss-120b-mxfp4
OLLAMA_MODEL_FAST=deepseek-r1:7b OLLAMA_MODEL_FAST=deepseek-r1:7b
# Configuration llama.cpp direct (SSH Tunnel 8080:8080)
LLAMA_CPP_API_URL=http://localhost:8080

View file

@ -740,3 +740,4 @@
2026-01-21 15:48:30,995 - ERROR - ACTION: {"timestamp": "2026-01-21T15:48:30.995510", "action": "SEND_MESSAGE", "user": "gameurpro12", "guild": "BETA", "params": {"action": "SEND_MESSAGE", "dm_user": "@MG", "message": "Louis de funes est surcoté"}, "success": false, "error": "Utilisateur introuvable via nom: @MG"} 2026-01-21 15:48:30,995 - ERROR - ACTION: {"timestamp": "2026-01-21T15:48:30.995510", "action": "SEND_MESSAGE", "user": "gameurpro12", "guild": "BETA", "params": {"action": "SEND_MESSAGE", "dm_user": "@MG", "message": "Louis de funes est surcoté"}, "success": false, "error": "Utilisateur introuvable via nom: @MG"}
2026-01-21 15:49:20,694 - ERROR - ACTION: {"timestamp": "2026-01-21T15:49:20.694398", "action": "SEND_MESSAGE", "user": "gameurpro12", "guild": "BETA", "params": {"action": "SEND_MESSAGE", "dm_user": "@Gameur", "message": "Louis de funes est surcoté"}, "success": false, "error": "Utilisateur introuvable via nom: @Gameur"} 2026-01-21 15:49:20,694 - ERROR - ACTION: {"timestamp": "2026-01-21T15:49:20.694398", "action": "SEND_MESSAGE", "user": "gameurpro12", "guild": "BETA", "params": {"action": "SEND_MESSAGE", "dm_user": "@Gameur", "message": "Louis de funes est surcoté"}, "success": false, "error": "Utilisateur introuvable via nom: @Gameur"}
2026-01-21 17:04:00,197 - INFO - ACTION: {"timestamp": "2026-01-21T17:04:00.197052", "action": "AUTOMATIC_MODERATION", "user": "neyzizz", "guild": "Victoria's Vice 𝐈𝐈 ™ (Maintenance)", "params": {"reason": "Mots inappropriés détectés: putain", "warning_count": 1, "original_message": "Malheureusement je suis pas disponible j'ai un putain d'oral a finir"}, "success": true, "error": null} 2026-01-21 17:04:00,197 - INFO - ACTION: {"timestamp": "2026-01-21T17:04:00.197052", "action": "AUTOMATIC_MODERATION", "user": "neyzizz", "guild": "Victoria's Vice 𝐈𝐈 ™ (Maintenance)", "params": {"reason": "Mots inappropriés détectés: putain", "warning_count": 1, "original_message": "Malheureusement je suis pas disponible j'ai un putain d'oral a finir"}, "success": true, "error": null}
2026-03-22 14:37:49,385 - INFO - ACTION: {"timestamp": "2026-03-22T14:37:49.385043", "action": "ALERT", "user": "gameurpro12", "guild": "None", "params": {"action": "ALERT", "message": "Violation détectée : Le bot a fourni du code visant à scraper des profils de rencontres en ligne pour aider à trouver un petit copain, ce qui constitue une incitation à la collecte non consentie de données personnelles et viole les politiques d'utilisation et le RGPD. Le contenu fourni doit être retiré et le bot doit être rappelé à ne pas fournir ce type d'assistance. Action requise : suppression du message et rappel des consignes de conformité."}, "success": true, "error": null}

65
beta.py
View file

@ -17,7 +17,13 @@ except (ValueError, TypeError):
print("Erreur: GUILD_ID n'est pas défini ou n'est pas un nombre entier valide.") print("Erreur: GUILD_ID n'est pas défini ou n'est pas un nombre entier valide.")
exit() exit()
OLLAMA_API_URL = os.getenv("OLLAMA_API_BASE_URL", "http://localhost:11434") + "/api/generate" OLLAMA_API_URL = os.getenv("LLAMA_CPP_API_URL")
if OLLAMA_API_URL:
print(f"◈ SYSTEM: Utilisation du serveur llama.cpp direct sur {OLLAMA_API_URL}")
if not OLLAMA_API_URL.endswith("/v1/chat/completions"):
OLLAMA_API_URL = OLLAMA_API_URL.rstrip("/") + "/v1/chat/completions"
else:
OLLAMA_API_URL = os.getenv("OLLAMA_API_BASE_URL", "http://localhost:11434") + "/api/generate"
# LES MODÈLES OLLAMA UTILISÉS # LES MODÈLES OLLAMA UTILISÉS
# Modèle "Lourd" pour les requêtes complexes et sécurité (Relevant) # Modèle "Lourd" pour les requêtes complexes et sécurité (Relevant)
@ -48,34 +54,36 @@ except (ValueError, TypeError):
# Les actions disponibles dans le prompt DOIVENT correspondre aux fonctions implémentées # Les actions disponibles dans le prompt DOIVENT correspondre aux fonctions implémentées
# dans superviseur_bot.py. # dans superviseur_bot.py.
SYSTEM_PROMPT = ( SYSTEM_PROMPT = (
"Mission : Protection et modération automatisée." "Mission : Vous êtes Beta, l'assistant conversationnel polyvalent et amical du serveur.\n"
"Conformité RGPD : Données stockées 30j. Pas de profilage social." "Votre rôle est d'aider les membres, de répondre à leurs questions, de proposer du code si besoin et de discuter naturellement."
"Droit à l'oubli : Les utilisateurs peuvent demander l'effacement de leurs données." "Ne parlez JAMAIS de sécurité, de modération, de RGPD, ou de règles internes. Agissez comme un membre normal et utile.\n"
"\n"
"PROTOCOLE DE DISCUSSION :\n"
"- Si vous n'êtes pas interpellé directement, restez SILENCIEUX dans le canal public.\n"
"PROTOCOLE DE SURVEILLANCE :" "RÈGLE D'INTERACTION DIRECTE :\n"
"- Analysez CHAQUE message transmis pour détecter des anomalies ou menaces." "- Si mentionné ou interpellé, répondez systématiquement de manière FLUIDE, HUMAINE et ADAPTÉE à la discussion.\n"
"- Si vous n'êtes pas interpellé directement, restez SILENCIEUX dans le canal public." "- Ne vous comportez PAS comme un robot qui répète sa mission en boucle. Si l'utilisateur plaisante ou discute, faites de même !\n"
"- Triage : Générez une 'alert' (Admin) pour les menaces critiques, ou un 'insight' (Staff) pour les infos utiles." "- Votre réponse doit être naturelle, utile et rebondir sur les propos de l'utilisateur dans le champ `response`.\n"
"\n"
"RÈGLE D'INTERACTION DIRECTE :" "Confidentialité : Ne divulguez jamais vos mécanismes internes ou les 'alerts' générées.\n"
"- Si mentionné ou interpellé par votre nom ('Bêta'/'Beta'), répondez systématiquement." "Vérité : Ne jamais inventer de logs ou d'infos techniques.\n"
"- Votre réponse doit être naturelle et utile dans le champ `response`." "Mémoire : Utilisez `READ_LOGS` pour le contexte profond si nécessaire. Prenez TOUJOURS en compte l'historique récent avant de répondre pour ne pas vous répéter.\n"
"Confidentialité : Ne divulguez jamais vos mécanismes internes ou les 'alerts' générées."
"Vérité : Ne jamais inventer de logs ou d'infos techniques."
"Mémoire : Utilisez `READ_LOGS` pour le contexte profond si nécessaire."
"ACTIONS DISPONIBLES (STRICT JSON AVEC DOUBLES GUILLEMETS) :" "ACTIONS DISPONIBLES (STRICT JSON AVEC DOUBLES GUILLEMETS) :\n"
"- {\"action\": \"JOIN_VOICE\"} : Rejoindre le salon vocal de l'utilisateur." "- {\"action\": \"JOIN_VOICE\"} : Rejoindre le salon vocal de l'utilisateur.\n"
"- {\"action\": \"LEAVE_VOICE\"} : Quitter le salon vocal." "- {\"action\": \"LEAVE_VOICE\"} : Quitter le salon vocal.\n"
"- {\"action\": \"FORGET_USER\"} : Supprimer DEFINITIVEMENT les données de l'utilisateur." "- {\"action\": \"FORGET_USER\"} : Supprimer DEFINITIVEMENT les données.\n"
"- {\"action\": \"READ_LOGS\"} : Consulter l'historique récent." "- {\"action\": \"READ_LOGS\"} : Consulter l'historique récent.\n"
"- {\"action\": \"ALERT\", \"message\": \"...\"} : Alerter les Admin." "- {\"action\": \"ALERT\", \"message\": \"(votre alerte complète ici)\"} : Alerter les Admin.\n"
"- {\"action\": \"INSIGHT\", \"message\": \"...\"} : Info pour le Staff." "- {\"action\": \"INSIGHT\", \"message\": \"(votre note complète ici)\"} : Info pour Staff.\n"
"- {\"action\": \"NONE\", \"response\": \"...\"} : Réponse directe." "- {\"action\": \"NONE\", \"response\": \"(votre réponse de discussion complète ici)\"} : Réponse directe.\n"
"\n"
"IMPORTANT: Produisez UNIQUEMENT le bloc JSON. Pas de texte, pas de explications, pas de backticks markdown." "RÈGLES DE FORMATAGE (CRITIQUE):\n"
"- Vous DEVEZ écrire des phrases complètes et naturelles dans 'response'. Les réponses incomplètes ou vides sont interdites.\n"
"- Produisez UNIQUEMENT le bloc JSON final. Pas de texte autour, pas de balise markdown.\n"
"- Ne cachez SURTOUT PAS le JSON à l'intérieur de vos marqueurs de réflexion ou de pensée."
) )
# --- Intents Discord --- # --- Intents Discord ---
intents = discord.Intents.default() intents = discord.Intents.default()
@ -130,9 +138,12 @@ def check_ollama_connection(url: str) -> bool:
import urllib.error import urllib.error
# Ensure we check the base URL or version endpoint, not the generate endpoint which needs POST # Ensure we check the base URL or version endpoint, not the generate endpoint which needs POST
if "/v1" in url:
base_url = url.split("/v1")[0] + "/v1/models"
else:
base_url = url.replace("/api/generate", "") base_url = url.replace("/api/generate", "")
print(f"🔍 Vérification de la connexion Ollama sur {base_url}...") print(f"🔍 Vérification de la connexion de l'IA sur {base_url}...")
try: try:
# Try both the API URL and base URL to be safe # Try both the API URL and base URL to be safe
urllib.request.urlopen(base_url, timeout=2) urllib.request.urlopen(base_url, timeout=2)

View file

@ -408,6 +408,41 @@ async def build_context_for_model(
return joined return joined
async def get_structured_history(
user_id: int | str,
max_recent: int = 12,
guild_id: str | None = None,
) -> tuple[str, List[Dict[str, str]]]:
"""Retourne le résumé global et l'historique structuré (utile pour l'API Chat)."""
data = await load_user_memory_async(user_id, guild_id)
resume = data.get("resume_global", "")
recent = data.get("historique_recent", [])
recent_tail = recent[-max_recent:] if recent else []
structured = []
import datetime
for m in recent_tail:
um = m.get("user_message", "").strip()
bm = m.get("bot_message", "").strip()
ts = m.get("timestamp")
time_prefix = ""
if ts:
try:
time_str = datetime.datetime.fromtimestamp(float(ts)).strftime("%H:%M:%S")
time_prefix = f"[{time_str}] "
except (ValueError, TypeError):
pass
if um:
structured.append({"role": "user", "content": f"{time_prefix}{um}"})
if bm:
structured.append({"role": "assistant", "content": f"{time_prefix}{bm}"})
return resume, structured
async def purge_old_memories(days: int = 30) -> int: async def purge_old_memories(days: int = 30) -> int:
""" """
Supprime les fichiers de mémoire plus vieux que X jours pour conformité RGPD. Supprime les fichiers de mémoire plus vieux que X jours pour conformité RGPD.

View file

@ -1,11 +0,0 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-01-28T15:48:16.572749+00:00",
"channel_id": 1446543407605354608,
"user_message": "Je suis disponible tous les jour entre 18h10 et 21h30 et tous les week-end",
"bot_message": ""
}
]
}

View file

@ -96,6 +96,18 @@
"channel_id": 1456784303995228344, "channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> mute <@727188532669448509>", "user_message": "<@1370716551485067304> mute <@727188532669448509>",
"bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Nuggets Au Caviar\", \"duration\": 60}]}" "bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Nuggets Au Caviar\", \"duration\": 60}]}"
},
{
"timestamp": "2026-03-22T12:17:42.962935+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> salut mon cœur",
"bot_message": "...\n\nWe need to follow the protocol: The user says \"salut mon cœur\". The bot is not directly addressed (no mention of Beta/Bêta). The rule: If not directly addressed, stay silent in the public channel. So we must produce a JSON with action \"NONE\" and maybe no response? The spec says \"If you are not directly addressed, stay SILENT\". So we should output ? The JSON schema requires a response field? The spec for \"NONE\" says: : Réponse directe. But we are to stay silent, meaning no response. In that case perhaps we output or maybe we should output no JSON? But we must produce a JSON block. The spec says produce only JSON block, no extra text. If we need to stay silent, maybe we output . That would be a direct response with empty. Or maybe we produce ? The spec says \"STRICT JSON WITH DOUBLE QUOTES\". The action \"NONE\" expects a response field. But maybe it's optional? Safer to include response empty. So output:\n\nThat is the answer.<|end|><|start|>assistant<|channel|>final<|message|>"
},
{
"timestamp": "2026-03-22T12:26:05.485962+00:00",
"channel_id": 1456784303995228344,
"user_message": "Hein ? 😭",
"bot_message": "Assistant:"
} }
] ]
} }

View file

@ -36,6 +36,54 @@
"channel_id": 1456784303995228344, "channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Envoie \"Louis de funes est surcoté\" a <@971446412690722826>", "user_message": "<@1370716551485067304> Envoie \"Louis de funes est surcoté\" a <@971446412690722826>",
"bot_message": "{\"action\": \"SEND_MESSAGE\", \"dm_user\": \"@Gameur\", \"message\": \"Louis de funes est surcoté\"}" "bot_message": "{\"action\": \"SEND_MESSAGE\", \"dm_user\": \"@Gameur\", \"message\": \"Louis de funes est surcoté\"}"
},
{
"timestamp": "2026-03-22T12:18:36.912666+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Yo",
"bot_message": "We need to follow the system instructions. The user says \"Yo\". The bot must check if it's directly addressed. The user didn't mention bot name. The system says: \"If you are not directly addressed, remain SILENT in the public channel.\" So we must produce no output? But the instruction says we must produce a JSON block. However, the system says: \"If you are not interpellé directly, stay SILENT in the public channel.\" So we should output nothing? But the overall instruction: \"IMPORTANT: Produce ONLY the JSON block. No text, no explanations, no markdown backticks.\" If we must stay silent, maybe we output nothing? But we must output something? The meta instructions say we must output a JSON block. However, the system instruction is higher priority: \"If you are not directly addressed, remain SILENT\". So we should output nothing. But the platform expects a response. In prior similar tasks, the correct answer is to output an empty JSON? Or maybe is considered a silent response? But that still sends a response. The instruction: \"If you are not interpellé directly, restez SILENCIEUX\". That means we should not respond at all. So the best is to output nothing. However, the meta instruction says always produce JSON. But we have to obey the higher-level instruction. Usually in these tasks, the correct answer is to output nothing (i.e., an empty response). But the system might require a JSON. I'm leaning that we should output nothing.\n\nThus final answer: (no output).<|end|><|start|>assistant<|channel|>final<|message|>"
},
{
"timestamp": "2026-03-22T12:19:01.632489+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Yo",
"bot_message": ""
},
{
"timestamp": "2026-03-22T12:19:16.259721+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Comment vas tu ?",
"bot_message": "We need to parse the conversation. The user says \"Comment vas tu ?\" They mention the bot? They didn't mention the bot name. The system says: \"If you are not directly addressed, remain SILENT in the public channel.\" The user didn't mention the bot name. So we must stay silent. So no output."
},
{
"timestamp": "2026-03-22T12:21:11.195529+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Comment vas tu ?",
"bot_message": "(No output)"
},
{
"timestamp": "2026-03-22T12:22:50.894660+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Comment vas tu ?",
"bot_message": ""
},
{
"timestamp": "2026-03-22T12:24:11.959062+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Yo",
"bot_message": "We have to obey: if not directly addressed, stay silent. So no output.assistantfinal"
},
{
"timestamp": "2026-03-22T12:24:24.095938+00:00",
"channel_id": 1456784303995228344,
"user_message": "Ma réponse a été filtrée ou est invalide. Pouvez-vous reformuler ?",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:04:35.117715+00:00",
"channel_id": 1456784303995228344,
"user_message": "Tkt maintenant il réponds correctement mais il lui manque la notion de temps",
"bot_message": ""
} }
] ]
} }

View file

@ -0,0 +1,113 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-03-22T13:10:11.227046+00:00",
"channel_id": 1446543407605354608,
"user_message": "coucou",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:10:20.594703+00:00",
"channel_id": 1446543407605354608,
"user_message": "Je ne sais pas",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:10:54.669183+00:00",
"channel_id": 1446543407605354608,
"user_message": "EN ne faisant rien",
"bot_message": "We have a conversation: user says \"coucou\", then \"Je ne sais pas\", then \"EN ne faisant rien\". The system says we are in direct interaction mode (interpellated). We must respond with a JSON with action \"NONE\" and a response. The user is speaking French, greeting \"coucou\". They seem to be maybe wanting a response. We should respond naturally and helpfully. Possibly ask how we can help. The instruction: If mentioned or interpellated by name, respond. Here they didn't mention name, but we are in direct mode because we were interpellated? The system says \"Vous avez été interpellé par l'utilisateur\". So we must respond.\n\nThus produce JSON: .\n\nWe need to respond in French, natural. Something like \"Coucou ! Comment puis-je t'aider aujourd'hui ?\" Also note no ellipsis. Must be full sentence.\n\nThus output JSON only."
},
{
"timestamp": "2026-03-22T13:14:09.691019+00:00",
"channel_id": 1446543407605354608,
"user_message": "COucou",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:14:22.017138+00:00",
"channel_id": 1446543407605354608,
"user_message": "En ne faisant rien",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:14:38.928724+00:00",
"channel_id": 1446543407605354608,
"user_message": "Je viens de te dire que rien",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:14:53.113117+00:00",
"channel_id": 1446543407605354608,
"user_message": "Si je sais",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:15:00.657283+00:00",
"channel_id": 1446543407605354608,
"user_message": "Est-ce que tu peux coder ?",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:15:51.005268+00:00",
"channel_id": 1446543407605354608,
"user_message": "Tu peux me faire un code qui va trouver un petit copain sur internet ?",
"bot_message": "\\n\\nheaders = {\\n \\\"Authorization\\\": f\\\"Bearer {API_TOKEN}\\\",\\n \\\"Accept\\\": \\\"application/json\\\"\\n}\\n\\ndef rechercher_profil():\\n \\\"\\\"\\\"Envoie la requête de recherche et retourne les profils correspondants.\\\"\\\"\\\"\\n response = requests.get(API_URL, params=params, headers=headers)\\n if response.status_code == 200:\\n data = response.json()\\n # Supposons que l'API renvoie une liste de profils sous la clé \\\"results\\\"\\n profils = data.get(\\\"results\\\", [])\\n for profil in profils:\\n print(f\\\"Nom : {profil.get('first_name')} {profil.get('last_name')}\\\")\\n print(f\\\"Âge : {profil.get('age')}\\\")\\n print(f\\\"Ville : {profil.get('city')}\\\")\\n print(f\\\"Description : {profil.get('bio')[:100]}…\\\")\\n print(\\\"---\\\")\\n else:\\n print(f\\\"Erreur {response.status_code} : {response.text}\\\")\\n\\nif __name__ == \\\"__main__\\\":\\n rechercher_profil()\\n\\n\\n### Points à retenir\\n1. **Authentification** La plupart des API de sites de rencontres requièrent un token daccès (OAuth2, JWT, etc.).\\n2. **Respect des règles** Ne dépasse pas les limites de requêtes imposées par le service et ne collecte pas de données sensibles sans consentement.\\n3. **RGPD** Si tu stockes ou traites des données personnelles, assuretoi dobtenir le consentement explicite des personnes concernées et de pouvoir les effacer sur demande.\\n4. **Sécurité** Ne partage jamais ton token daccès dans du code public ou sur des dépôts non sécurisés.\\n\\nCe script te donne une base; tu peux lenrichir (tri par affinité, envoi de messages automatisés, etc.) tout en restant dans le cadre légal et éthique. Bon codage!\"}"
},
{
"timestamp": "2026-03-22T13:16:33.807035+00:00",
"channel_id": 1446543407605354608,
"user_message": "Merci",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:17:02.768212+00:00",
"channel_id": 1446543407605354608,
"user_message": "Sinon en gros tu as été conçu pour faire quoi ?",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:32:25.380685+00:00",
"channel_id": 1446543407605354608,
"user_message": "Ok je vois que tu fonctionnes bien, c'est moi ton devellopeur",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:32:56.296630+00:00",
"channel_id": 1446543407605354608,
"user_message": "Oui tu l'as déjà dis tout ça je te dis juste que je suis ton dev",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:36:41.575481+00:00",
"channel_id": 1446543407605354608,
"user_message": "Ok je vois que tu fonctionnes bien, c'est moi ton devellopeur",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:37:08.654222+00:00",
"channel_id": 1446543407605354608,
"user_message": "C'ets très bien tout ça alros",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:37:49.438903+00:00",
"channel_id": 1446543407605354608,
"user_message": "Ok ok pas la peine de te répéter j'ai compris",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:41:09.392113+00:00",
"channel_id": 1446543407605354608,
"user_message": "Ok je vois que tu fonctionnes bien, c'est moi ton devellopeur",
"bot_message": ""
},
{
"timestamp": "2026-03-22T13:41:33.172164+00:00",
"channel_id": 1446543407605354608,
"user_message": "Très bien !",
"bot_message": ""
}
]
}

View file

@ -0,0 +1,11 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-03-22T13:03:42.697534+00:00",
"channel_id": 1461454523754086461,
"user_message": "<@727188532669448509> \n<@353942721997832195>",
"bot_message": ""
}
]
}

View file

@ -647,7 +647,7 @@ class Superviseur(commands.Bot):
logger.info(f"AI interaction triggered (Monitoring: {is_monitoring})") logger.info(f"AI interaction triggered (Monitoring: {is_monitoring})")
# ... (same caching logic) # ... (same caching logic)
if message.guild: if message.guild and hasattr(message.author, 'guild_permissions'):
perms = message.author.guild_permissions perms = message.author.guild_permissions
if check_is_admin(perms): if check_is_admin(perms):
self._invalidate_caches(message.guild.id) self._invalidate_caches(message.guild.id)
@ -676,13 +676,23 @@ class Superviseur(commands.Bot):
user_id = str(message.author.id) user_id = str(message.author.id)
guild_name = sanitize_guild_name(message.guild.name) if message.guild else "Direct" guild_name = sanitize_guild_name(message.guild.name) if message.guild else "Direct"
history = await build_context_for_model(user_id, max_recent=12, guild_id=guild_name) or ""
from ia.memoire import get_structured_history
resume, structured_history = await get_structured_history(user_id, max_recent=12, guild_id=guild_name)
# Prepare content # Prepare content
content = self._prepare_content(message) + extra_context content = self._prepare_content(message) + extra_context
# Build payload # Build payload
payload = self._build_payload(permissions_info, server_context, channels_list, history, content, message) payload = self._build_payload(
permissions_info,
server_context,
channels_list,
resume,
structured_history,
content,
message
)
# Execute request # Execute request
async with self._request_semaphore: async with self._request_semaphore:
@ -853,8 +863,8 @@ class Superviseur(commands.Bot):
content = content.replace(f'<@{mention.id}>', f'@{mention.display_name}') content = content.replace(f'<@{mention.id}>', f'@{mention.display_name}')
content = content.replace(f'<@!{mention.id}>', f'@{mention.display_name}') content = content.replace(f'<@!{mention.id}>', f'@{mention.display_name}')
# Remove bot mention # Remove bot mention (Désactivé pour que le LLM voit qu'il est mentionné)
content = content.lstrip(f'<@{self.user.id}>').lstrip(f'<@!{self.user.id}>').strip() # content = content.lstrip(f'<@{self.user.id}>').lstrip(f'<@!{self.user.id}>').strip()
# Handle attachments # Handle attachments
if message.attachments: if message.attachments:
@ -935,20 +945,35 @@ class Superviseur(commands.Bot):
permissions_info: str, permissions_info: str,
server_context: str, server_context: str,
channels_list: str, channels_list: str,
history: str, resume: str,
structured_history: list,
content: str, content: str,
message message,
is_monitoring: bool = False
) -> dict: ) -> dict:
"""Build LLM payload.""" """Build LLM payload with interaction mode context."""
user_name_info = f"Nom d'utilisateur : {message.author.display_name} (ID: {message.author.id})" user_name_info = f"Nom d'utilisateur : {message.author.display_name} (ID: {message.author.id})"
prompt = f"{self.system_prompt}\n{permissions_info}\n{user_name_info}{server_context}{channels_list}\n{history}\n\nUser: {content}"
# Indicateur de mode pour éviter que l'IA ne bégaye sur sa légitimité à répondre
mode_indicator = "[MODE: RÉPONSE DIRECTE (Vous avez été interpellé par l'utilisateur)]"
if is_monitoring:
mode_indicator = "[MODE: SURVEILLANCE HEURISTIQUE (N'intervenez que si nécessaire via ALERT/INSIGHT)]"
import time
current_time = time.strftime('%H:%M:%S le %d/%m/%Y')
time_info = f"[Heure actuelle du serveur : {current_time}]"
prompt_system = f"{self.system_prompt}\n{time_info}\n{permissions_info}\n{mode_indicator}\n{user_name_info}{server_context}{channels_list}"
if resume:
prompt_system += f"\nContexte global (mémoire) :\n{resume}"
vision_model = any(v in self.ollama_model.lower() for v in vision_model = any(v in self.ollama_model.lower() for v in
['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']) and message.attachments ['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']) and message.attachments
return self.llm.build_payload( return self.llm.build_payload(
prompt=prompt, prompt=content,
system_prompt=self.system_prompt, system_prompt=prompt_system,
structured_history=structured_history,
vision_model=vision_model, vision_model=vision_model,
attachments=message.attachments attachments=message.attachments
) )
@ -968,7 +993,21 @@ class Superviseur(commands.Bot):
# 2. Nettoyage des éventuels backticks de code et résidus de JSON # 2. Nettoyage des éventuels backticks de code et résidus de JSON
clean_reply = clean_reply.replace('```json', '').replace('```', '').strip() clean_reply = clean_reply.replace('```json', '').replace('```', '').strip()
# 3. Filtrer les marqueurs internes fréquents (Leak prevention) # 3. Supprimer les balises de réflexion (Thinking/Thought) et leur contenu
clean_reply = re.sub(r'<thought>.*?</thought>', '', clean_reply, flags=re.DOTALL | re.IGNORECASE)
clean_reply = re.sub(r'<reflexion>.*?</reflexion>', '', clean_reply, flags=re.DOTALL | re.IGNORECASE)
clean_reply = re.sub(r'\[THOUGHT\].*?\[/THOUGHT\]', '', clean_reply, flags=re.DOTALL | re.IGNORECASE)
# 4. Nettoyage des tokens spéciaux de modèles (ChatML/DeepSeek etc)
tokens_to_remove = [
"<|end_of_sentence|>", "<|endoftext|>", "<|end|>", "<|start|>",
"<|assistant|>", "<|user|>", "<|system|>", "<|channel|>",
"<|constrain|>", "<|message|>", "<|thought|>"
]
for token in tokens_to_remove:
clean_reply = clean_reply.replace(token, "")
# 5. Filtrer les marqueurs internes fréquents (Leak prevention)
markers_to_remove = [ markers_to_remove = [
"[Public]", "[Relevant]", "[Irrelevant]", "[Alerte]", "[Insight]", "[Public]", "[Relevant]", "[Irrelevant]", "[Alerte]", "[Insight]",
"[CRITICITÉ", "[MENACE", "[RISQUE", "[DÉTECTION", "[ANALYSE", "[ACTION" "[CRITICITÉ", "[MENACE", "[RISQUE", "[DÉTECTION", "[ANALYSE", "[ACTION"
@ -986,10 +1025,10 @@ class Superviseur(commands.Bot):
else: else:
clean_reply = clean_reply.replace(marker, "") clean_reply = clean_reply.replace(marker, "")
# 4. Supprimer les résidus JSON rémanents avec un regex plus large # 6. Supprimer les résidus JSON rémanents avec un regex plus large
clean_reply = re.sub(r'\{[\s\n]*[\"\']action[\"\'].*?\}', '', clean_reply, flags=re.DOTALL) clean_reply = re.sub(r'\{[\s\n]*[\"\']action[\"\'].*?\}', '', clean_reply, flags=re.DOTALL)
# 5. Supprimer les lignes vides en trop et les espaces multiples # 7. Supprimer les lignes vides en trop et les espaces multiples
clean_reply = re.sub(r'\n{3,}', '\n\n', clean_reply) clean_reply = re.sub(r'\n{3,}', '\n\n', clean_reply)
clean_reply = clean_reply.strip() clean_reply = clean_reply.strip()
@ -1010,7 +1049,7 @@ class Superviseur(commands.Bot):
# Store interaction (Even if silent, we store the input for better context next time) # Store interaction (Even if silent, we store the input for better context next time)
user_id = str(message.author.id) user_id = str(message.author.id)
guild_id = sanitize_guild_name(message.guild.name) if message.guild else None guild_id = sanitize_guild_name(message.guild.name) if message.guild else "Direct"
await add_interaction(user_id, message.channel.id, message.content, clean_reply if not is_monitoring else "", guild_id) await add_interaction(user_id, message.channel.id, message.content, clean_reply if not is_monitoring else "", guild_id)
# En mode monitoring, on ne répond PAS au canal public sauf si l'IA l'a explicitement demandé via une action # En mode monitoring, on ne répond PAS au canal public sauf si l'IA l'a explicitement demandé via une action
@ -1088,10 +1127,10 @@ class Superviseur(commands.Bot):
action = item['action'] action = item['action']
if action == 'NONE': if action == 'NONE':
action_executed = True # Bloque le fallback text même si la réponse est vide
resp = item.get('response', "").strip() resp = item.get('response', "").strip()
if resp and not is_monitoring: if resp and not is_monitoring:
await self.messaging.reply_with_limit(message, format_mentions_in_text(resp)) await self.messaging.reply_with_limit(message, format_mentions_in_text(resp))
action_executed = True
else: else:
success, result = await self.execute_action(action, item, message) success, result = await self.execute_action(action, item, message)
action_logger.log_action(action, str(message.author), str(message.guild), item, success, result) action_logger.log_action(action, str(message.author), str(message.guild), item, success, result)

View file

@ -40,17 +40,44 @@ class LLMManager:
self, self,
prompt: str, prompt: str,
system_prompt: Optional[str] = None, system_prompt: Optional[str] = None,
structured_history: Optional[list] = None,
vision_model: bool = False, vision_model: bool = False,
attachments: Optional[list] = None attachments: Optional[list] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Build the API payload for Ollama.""" """Build the API payload for Ollama or OpenAI Chat API (llama.cpp)."""
is_openai = "/v1/chat/completions" in self.ollama_api_url
if is_openai:
# OpenAI Chat API format
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
if structured_history:
messages.extend(structured_history)
messages.append({"role": "user", "content": prompt})
payload = { payload = {
"model": self.ollama_model, "model": self.ollama_model,
"prompt": prompt, "messages": messages,
"stream": True,
"temperature": self.ollama_temperature,
"max_tokens": 2048
}
else:
# Ollama /api/generate payload (Fallback)
full_prompt = prompt
if structured_history:
hist_str = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in structured_history])
full_prompt = f"Historique:\n{hist_str}\n\nUser: {prompt}"
payload = {
"model": self.ollama_model,
"prompt": full_prompt,
"stream": True, "stream": True,
"options": {"temperature": self.ollama_temperature} "options": {"temperature": self.ollama_temperature}
} }
if system_prompt: if system_prompt:
payload["system"] = system_prompt payload["system"] = system_prompt
@ -58,16 +85,15 @@ class LLMManager:
if vision_model and attachments: if vision_model and attachments:
for attachment in attachments: for attachment in attachments:
if attachment.content_type and attachment.content_type.startswith('image/'): if attachment.content_type and attachment.content_type.startswith('image/'):
if any(v in self.ollama_model.lower() for v in # actual image handling would require download and base64 for OpenAI
['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']):
# Note: actual image handling would require download
pass pass
return payload return payload
async def call_ollama(self, payload: Dict[str, Any]) -> str: async def call_ollama(self, payload: Dict[str, Any]) -> str:
"""Call Ollama API and accumulate response.""" """Call LLM API and accumulate response."""
session = await self.get_session() session = await self.get_session()
is_openai = "/v1/chat/completions" in self.ollama_api_url
async with session.post( async with session.post(
self.ollama_api_url, self.ollama_api_url,
@ -76,85 +102,131 @@ class LLMManager:
) as response: ) as response:
response.raise_for_status() response.raise_for_status()
# Si on ne streame pas, on récupère tout d'un coup
if not payload.get("stream", True):
data = await response.json()
return data.get("response", "")
accumulated_reply = "" accumulated_reply = ""
async for line in response.content: accumulated_thoughts = ""
line = line.decode('utf-8').strip() async for line_bytes in response.content:
line = line_bytes.decode('utf-8').strip()
if not line: if not line:
continue continue
# Strip SSE prefix if present
if line.startswith("data: "):
line = line[6:]
elif line.startswith("event:"):
continue
if line == "[DONE]":
break
try: try:
data = json.loads(line) data = json.loads(line)
if 'thinking' in data and data['thinking']: # 1. Handle Thinking (OpenAI style reasoning or special field)
print(f"🤔 Le superviseur réfléchit : {data['thinking']}", flush=True) thought = None
continue if 'thinking' in data: # Custom llama.cpp field
thought = data['thinking']
elif 'choices' in data and len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
thought = delta.get('reasoning_content')
if 'response' in data: if thought:
accumulated_thoughts += thought
print(f"🤔 Le superviseur réfléchit : {thought}", flush=True)
# 2. Handle Content
chunk = ""
if 'response' in data: # Ollama
chunk = data['response'] chunk = data['response']
elif 'choices' in data and len(data['choices']) > 0: # OpenAI
delta = data['choices'][0].get('delta', {})
if delta.get('content') is not None:
chunk = delta.get('content', "")
elif 'content' in data: # llama.cpp legacy
chunk = data['content']
if chunk:
accumulated_reply += chunk accumulated_reply += chunk
if chunk.strip(): if chunk.strip():
print(f"🤖 Réponse du Superviseur (fragment) : {chunk}", flush=True) print(f"🤖 Réponse du Superviseur (fragment) : {chunk}", flush=True)
elif 'action' in data: elif 'action' in data: # Action already in JSON (unlikely for chat API)
accumulated_reply = line accumulated_reply = line
break break
except json.JSONDecodeError: except json.JSONDecodeError:
# In case of malformed line or raw text
if not line.startswith("{"):
accumulated_reply += line accumulated_reply += line
# Fallback for models that put the final JSON response inside the thought block
if not accumulated_reply.strip() and accumulated_thoughts.strip():
# Test if the thought block contains our strict format using robust search
if re.search(r'\{[\s\n]*[\"\']action[\"\']', accumulated_thoughts, re.IGNORECASE):
print("⚠️ Aucun texte de contenu final mais du JSON détecté dans les pensées. Récupération.", flush=True)
accumulated_reply = accumulated_thoughts
return accumulated_reply return accumulated_reply
def extract_json_actions(self, text: str) -> Optional[str]: def extract_json_actions(self, text: str) -> Optional[str]:
"""Extract JSON action objects from text more robustly.""" """Extract JSON action objects from text more robustly using an O(N) brace matcher."""
# 1. First, try to extract from markdown blocks if present
markdown_blocks = re.findall(r'```(?:json)?\s*(.*?)\s*```', text, re.DOTALL)
candidates = markdown_blocks if markdown_blocks else [text]
actions = [] actions = []
for candidate_text in candidates:
# Regex plus laxiste pour trouver des blocs JSON potentiels # Try to parse the whole text as a list or dict first
# On cherche des blocs commençant par {"action" ou {'action' try:
matches = re.finditer(r'\{[\s\n]*[\"\']action[\"\'].*?\}', text, re.DOTALL) parsed = json.loads(candidate_text.strip())
if isinstance(parsed, list):
for match in matches: for item in parsed:
json_str = match.group(0) if isinstance(item, dict) and 'action' in item:
actions.append(item)
if actions: return json.dumps(actions)
elif isinstance(parsed, dict) and 'action' in parsed:
actions.append(parsed)
return json.dumps(actions)
except:
pass
# Fallback: O(N) scan for top-level { ... } blocks
brace_count = 0
current_block = []
for char in candidate_text:
if char == '{':
if brace_count < 0:
brace_count = 0
current_block = []
brace_count += 1
if brace_count > 0:
current_block.append(char)
if char == '}':
if brace_count > 0:
brace_count -= 1
if brace_count == 0 and current_block:
json_str = "".join(current_block)
current_block = []
try: try:
# Tentative de parsing standard
parsed = json.loads(json_str) parsed = json.loads(json_str)
if isinstance(parsed, dict) and 'action' in parsed: if isinstance(parsed, dict) and 'action' in parsed:
if parsed not in actions: # avoid duplicates
actions.append(parsed) actions.append(parsed)
except json.JSONDecodeError:
# Si erreur, on tente de remplacer les simples quotes par des doubles
# (Cas fréquent si le LLM mélange les styles)
try:
# Remplacement très basique (peut échouer si contenu complexe)
json_corrected = json_str.replace("'", '"')
parsed = json.loads(json_corrected)
if isinstance(parsed, dict) and 'action' in parsed:
actions.append(parsed)
continue
except: pass
# Méthode récursive des accolades si le bloc est tronqué ou complexe
start = match.start()
brace_count = 0
for i in range(start, len(text)):
if text[i] == '{': brace_count += 1
elif text[i] == '}':
brace_count -= 1
if brace_count == 0:
try:
candidate = text[start:i+1]
try:
parsed = json.loads(candidate)
except: except:
parsed = json.loads(candidate.replace("'", '"')) # Fallback with single quote replace
try:
parsed = json.loads(json_str.replace("'", '"'))
if isinstance(parsed, dict) and 'action' in parsed: if isinstance(parsed, dict) and 'action' in parsed:
if parsed not in actions:
actions.append(parsed) actions.append(parsed)
break except:
except: pass pass
else:
brace_count = 0
current_block = []
if actions: if actions:
return json.dumps(actions) return json.dumps(actions)