Optimisation performance (30t/s --> 50t/s) & adaptation parsing json --> llama.cpp & Fix système mémoire
This commit is contained in:
parent
e27b60e60c
commit
0ba47f8363
19 changed files with 457 additions and 123 deletions
5
.env
5
.env
|
|
@ -11,5 +11,8 @@ PRIMARY_ASSETS_IDS=971446412690722826
|
|||
INTROSPECTION_CHANNEL_ID=1461107327381016629
|
||||
|
||||
# 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
|
||||
|
||||
# Configuration llama.cpp direct (SSH Tunnel 8080:8080)
|
||||
LLAMA_CPP_API_URL=http://localhost:8080
|
||||
|
|
@ -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: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-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}
|
||||
|
|
|
|||
67
beta.py
67
beta.py
|
|
@ -17,7 +17,13 @@ except (ValueError, TypeError):
|
|||
print("Erreur: GUILD_ID n'est pas défini ou n'est pas un nombre entier valide.")
|
||||
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
|
||||
# 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
|
||||
# dans superviseur_bot.py.
|
||||
SYSTEM_PROMPT = (
|
||||
"Mission : Protection et modération automatisée."
|
||||
"Conformité RGPD : Données stockées 30j. Pas de profilage social."
|
||||
"Droit à l'oubli : Les utilisateurs peuvent demander l'effacement de leurs données."
|
||||
"Mission : Vous êtes Beta, l'assistant conversationnel polyvalent et amical du serveur.\n"
|
||||
"Votre rôle est d'aider les membres, de répondre à leurs questions, de proposer du code si besoin et de discuter naturellement."
|
||||
"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 :"
|
||||
"- Analysez CHAQUE message transmis pour détecter des anomalies ou menaces."
|
||||
"- Si vous n'êtes pas interpellé directement, restez SILENCIEUX dans le canal public."
|
||||
"- Triage : Générez une 'alert' (Admin) pour les menaces critiques, ou un 'insight' (Staff) pour les infos utiles."
|
||||
|
||||
"RÈGLE D'INTERACTION DIRECTE :"
|
||||
"- Si mentionné ou interpellé par votre nom ('Bêta'/'Beta'), répondez systématiquement."
|
||||
"- Votre réponse doit être naturelle et utile dans le champ `response`."
|
||||
|
||||
"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."
|
||||
"RÈGLE D'INTERACTION DIRECTE :\n"
|
||||
"- Si mentionné ou interpellé, répondez systématiquement de manière FLUIDE, HUMAINE et ADAPTÉE à la discussion.\n"
|
||||
"- 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"
|
||||
"- Votre réponse doit être naturelle, utile et rebondir sur les propos de l'utilisateur dans le champ `response`.\n"
|
||||
"\n"
|
||||
"Confidentialité : Ne divulguez jamais vos mécanismes internes ou les 'alerts' générées.\n"
|
||||
"Vérité : Ne jamais inventer de logs ou d'infos techniques.\n"
|
||||
"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"
|
||||
|
||||
|
||||
"ACTIONS DISPONIBLES (STRICT JSON AVEC DOUBLES GUILLEMETS) :"
|
||||
"- {\"action\": \"JOIN_VOICE\"} : Rejoindre le salon vocal de l'utilisateur."
|
||||
"- {\"action\": \"LEAVE_VOICE\"} : Quitter le salon vocal."
|
||||
"- {\"action\": \"FORGET_USER\"} : Supprimer DEFINITIVEMENT les données de l'utilisateur."
|
||||
"- {\"action\": \"READ_LOGS\"} : Consulter l'historique récent."
|
||||
"- {\"action\": \"ALERT\", \"message\": \"...\"} : Alerter les Admin."
|
||||
"- {\"action\": \"INSIGHT\", \"message\": \"...\"} : Info pour le Staff."
|
||||
"- {\"action\": \"NONE\", \"response\": \"...\"} : Réponse directe."
|
||||
|
||||
"IMPORTANT: Produisez UNIQUEMENT le bloc JSON. Pas de texte, pas de explications, pas de backticks markdown."
|
||||
"ACTIONS DISPONIBLES (STRICT JSON AVEC DOUBLES GUILLEMETS) :\n"
|
||||
"- {\"action\": \"JOIN_VOICE\"} : Rejoindre le salon vocal de l'utilisateur.\n"
|
||||
"- {\"action\": \"LEAVE_VOICE\"} : Quitter le salon vocal.\n"
|
||||
"- {\"action\": \"FORGET_USER\"} : Supprimer DEFINITIVEMENT les données.\n"
|
||||
"- {\"action\": \"READ_LOGS\"} : Consulter l'historique récent.\n"
|
||||
"- {\"action\": \"ALERT\", \"message\": \"(votre alerte complète ici)\"} : Alerter les Admin.\n"
|
||||
"- {\"action\": \"INSIGHT\", \"message\": \"(votre note complète ici)\"} : Info pour Staff.\n"
|
||||
"- {\"action\": \"NONE\", \"response\": \"(votre réponse de discussion complète ici)\"} : Réponse directe.\n"
|
||||
"\n"
|
||||
"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.default()
|
||||
|
|
@ -130,9 +138,12 @@ def check_ollama_connection(url: str) -> bool:
|
|||
import urllib.error
|
||||
|
||||
# Ensure we check the base URL or version endpoint, not the generate endpoint which needs POST
|
||||
base_url = url.replace("/api/generate", "")
|
||||
if "/v1" in url:
|
||||
base_url = url.split("/v1")[0] + "/v1/models"
|
||||
else:
|
||||
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 both the API URL and base URL to be safe
|
||||
urllib.request.urlopen(base_url, timeout=2)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -408,6 +408,41 @@ async def build_context_for_model(
|
|||
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:
|
||||
"""
|
||||
Supprime les fichiers de mémoire plus vieux que X jours pour conformité RGPD.
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -96,6 +96,18 @@
|
|||
"channel_id": 1456784303995228344,
|
||||
"user_message": "<@1370716551485067304> mute <@727188532669448509>",
|
||||
"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:"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -36,6 +36,54 @@
|
|||
"channel_id": 1456784303995228344,
|
||||
"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é\"}"
|
||||
},
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
113
memoires/Direct/971446412690722826.json
Normal file
113
memoires/Direct/971446412690722826.json
Normal 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 d’accè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, assure‑toi d’obtenir le consentement explicite des personnes concernées et de pouvoir les effacer sur demande.\\n4. **Sécurité** – Ne partage jamais ton token d’accès dans du code public ou sur des dépôts non sécurisés.\\n\\nCe script te donne une base ; tu peux l’enrichir (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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -647,7 +647,7 @@ class Superviseur(commands.Bot):
|
|||
logger.info(f"AI interaction triggered (Monitoring: {is_monitoring})")
|
||||
|
||||
# ... (same caching logic)
|
||||
if message.guild:
|
||||
if message.guild and hasattr(message.author, 'guild_permissions'):
|
||||
perms = message.author.guild_permissions
|
||||
if check_is_admin(perms):
|
||||
self._invalidate_caches(message.guild.id)
|
||||
|
|
@ -676,13 +676,23 @@ class Superviseur(commands.Bot):
|
|||
|
||||
user_id = str(message.author.id)
|
||||
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
|
||||
content = self._prepare_content(message) + extra_context
|
||||
|
||||
# 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
|
||||
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}')
|
||||
|
||||
# Remove bot mention
|
||||
content = content.lstrip(f'<@{self.user.id}>').lstrip(f'<@!{self.user.id}>').strip()
|
||||
# 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()
|
||||
|
||||
# Handle attachments
|
||||
if message.attachments:
|
||||
|
|
@ -935,20 +945,35 @@ class Superviseur(commands.Bot):
|
|||
permissions_info: str,
|
||||
server_context: str,
|
||||
channels_list: str,
|
||||
history: str,
|
||||
resume: str,
|
||||
structured_history: list,
|
||||
content: str,
|
||||
message
|
||||
message,
|
||||
is_monitoring: bool = False
|
||||
) -> 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})"
|
||||
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
|
||||
['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']) and message.attachments
|
||||
|
||||
return self.llm.build_payload(
|
||||
prompt=prompt,
|
||||
system_prompt=self.system_prompt,
|
||||
prompt=content,
|
||||
system_prompt=prompt_system,
|
||||
structured_history=structured_history,
|
||||
vision_model=vision_model,
|
||||
attachments=message.attachments
|
||||
)
|
||||
|
|
@ -968,7 +993,21 @@ class Superviseur(commands.Bot):
|
|||
# 2. Nettoyage des éventuels backticks de code et résidus de JSON
|
||||
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 = [
|
||||
"[Public]", "[Relevant]", "[Irrelevant]", "[Alerte]", "[Insight]",
|
||||
"[CRITICITÉ", "[MENACE", "[RISQUE", "[DÉTECTION", "[ANALYSE", "[ACTION"
|
||||
|
|
@ -986,10 +1025,10 @@ class Superviseur(commands.Bot):
|
|||
else:
|
||||
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)
|
||||
|
||||
# 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 = 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)
|
||||
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)
|
||||
|
||||
# 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']
|
||||
if action == 'NONE':
|
||||
action_executed = True # Bloque le fallback text même si la réponse est vide
|
||||
resp = item.get('response', "").strip()
|
||||
if resp and not is_monitoring:
|
||||
await self.messaging.reply_with_limit(message, format_mentions_in_text(resp))
|
||||
action_executed = True
|
||||
else:
|
||||
success, result = await self.execute_action(action, item, message)
|
||||
action_logger.log_action(action, str(message.author), str(message.guild), item, success, result)
|
||||
|
|
|
|||
|
|
@ -40,34 +40,60 @@ class LLMManager:
|
|||
self,
|
||||
prompt: str,
|
||||
system_prompt: Optional[str] = None,
|
||||
structured_history: Optional[list] = None,
|
||||
vision_model: bool = False,
|
||||
attachments: Optional[list] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Build the API payload for Ollama."""
|
||||
payload = {
|
||||
"model": self.ollama_model,
|
||||
"prompt": prompt,
|
||||
"stream": True,
|
||||
"options": {"temperature": self.ollama_temperature}
|
||||
}
|
||||
"""Build the API payload for Ollama or OpenAI Chat API (llama.cpp)."""
|
||||
is_openai = "/v1/chat/completions" in self.ollama_api_url
|
||||
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
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 = {
|
||||
"model": self.ollama_model,
|
||||
"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,
|
||||
"options": {"temperature": self.ollama_temperature}
|
||||
}
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
|
||||
# Vision support (simplified)
|
||||
if vision_model and attachments:
|
||||
for attachment in attachments:
|
||||
if attachment.content_type and attachment.content_type.startswith('image/'):
|
||||
if any(v in self.ollama_model.lower() for v in
|
||||
['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']):
|
||||
# Note: actual image handling would require download
|
||||
pass
|
||||
# actual image handling would require download and base64 for OpenAI
|
||||
pass
|
||||
|
||||
return payload
|
||||
|
||||
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()
|
||||
is_openai = "/v1/chat/completions" in self.ollama_api_url
|
||||
|
||||
async with session.post(
|
||||
self.ollama_api_url,
|
||||
|
|
@ -76,85 +102,131 @@ class LLMManager:
|
|||
) as response:
|
||||
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 = ""
|
||||
async for line in response.content:
|
||||
line = line.decode('utf-8').strip()
|
||||
accumulated_thoughts = ""
|
||||
async for line_bytes in response.content:
|
||||
line = line_bytes.decode('utf-8').strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Strip SSE prefix if present
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
elif line.startswith("event:"):
|
||||
continue
|
||||
|
||||
if line == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
if 'thinking' in data and data['thinking']:
|
||||
print(f"🤔 Le superviseur réfléchit : {data['thinking']}", flush=True)
|
||||
continue
|
||||
# 1. Handle Thinking (OpenAI style reasoning or special field)
|
||||
thought = None
|
||||
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']
|
||||
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
|
||||
if chunk.strip():
|
||||
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
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
accumulated_reply += line
|
||||
# In case of malformed line or raw text
|
||||
if not line.startswith("{"):
|
||||
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
|
||||
|
||||
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 = []
|
||||
|
||||
# Regex plus laxiste pour trouver des blocs JSON potentiels
|
||||
# On cherche des blocs commençant par {"action" ou {'action'
|
||||
matches = re.finditer(r'\{[\s\n]*[\"\']action[\"\'].*?\}', text, re.DOTALL)
|
||||
|
||||
for match in matches:
|
||||
json_str = match.group(0)
|
||||
for candidate_text in candidates:
|
||||
# Try to parse the whole text as a list or dict first
|
||||
try:
|
||||
# Tentative de parsing standard
|
||||
parsed = json.loads(json_str)
|
||||
if isinstance(parsed, dict) and 'action' in parsed:
|
||||
parsed = json.loads(candidate_text.strip())
|
||||
if isinstance(parsed, list):
|
||||
for item in parsed:
|
||||
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)
|
||||
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
|
||||
return json.dumps(actions)
|
||||
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] == '}':
|
||||
# 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:
|
||||
if brace_count == 0 and current_block:
|
||||
json_str = "".join(current_block)
|
||||
current_block = []
|
||||
try:
|
||||
candidate = text[start:i+1]
|
||||
try:
|
||||
parsed = json.loads(candidate)
|
||||
except:
|
||||
parsed = json.loads(candidate.replace("'", '"'))
|
||||
|
||||
parsed = json.loads(json_str)
|
||||
if isinstance(parsed, dict) and 'action' in parsed:
|
||||
actions.append(parsed)
|
||||
break
|
||||
except: pass
|
||||
if parsed not in actions: # avoid duplicates
|
||||
actions.append(parsed)
|
||||
except:
|
||||
# Fallback with single quote replace
|
||||
try:
|
||||
parsed = json.loads(json_str.replace("'", '"'))
|
||||
if isinstance(parsed, dict) and 'action' in parsed:
|
||||
if parsed not in actions:
|
||||
actions.append(parsed)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
brace_count = 0
|
||||
current_block = []
|
||||
|
||||
if actions:
|
||||
return json.dumps(actions)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue