Merge branch 'Eliott' into 'main'

Eliott

See merge request Omega_Kube/beta!3
This commit is contained in:
Gameur 2026-06-17 19:50:37 +02:00
commit 5b71c9fe28
9415 changed files with 3095 additions and 1899580 deletions

18
.env
View file

@ -1,18 +0,0 @@
DISCORD_TOKEN=MTM3MDcxNjU1MTQ4NTA2NzMwNA.GJHUjY.YrzGaD2yXj0-UwYZ0ut7RA_tSeI5ji1nD5iLTY
GUILD_ID=1370716551485067304
# --- CONFIGURATION BÊTA ---
# IDs des Administrateurs (Séparés par des virgules)
ADMIN_IDS=971446412690722826
# IDs du Staff (Assets Primaires), séparés par des virgules
PRIMARY_ASSETS_IDS=971446412690722826
# Channel d'Introspection (Log Stream de Bêta)
INTROSPECTION_CHANNEL_ID=1461107327381016629
# Modèles Ollama (Optimisation Radeon 8060S 64GB)
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

79
.gitignore vendored Executable file → Normal file
View file

@ -1,28 +1,75 @@
# Fichiers d'environnement (contiennent des tokens/clés sensibles)
# ============================
# Environment & Secrets
# ============================
.env
# Fichiers de configuration générés
config.json
whitelist.json
bridges.json
config.db
malware.json
.env.*
!.env.example
# Cache Python
# ============================
# Python
# ============================
__pycache__/
*.pyc
*.pyo
*.pyd
*.py[cod]
*$py.class
*.so
*.egg-info/
*.egg
dist/
build/
*.whl
# Environnements virtuels
# ============================
# Virtual Environment
# ============================
venv/
env/
.venv/
env/
ENV/
# IDE
# ============================
# IDE / Editor
# ============================
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
# ============================
# Logs
*.Logs
# ============================
*.log
.data
logs/
# ============================
# Data (runtime / dynamic)
# ============================
data/
*.db
*.sqlite
*.sqlite3
# ============================
# AI Models (fichiers volumineux)
# ============================
model/
*.gguf
*.bin
*.onnx
*.pt
*.pth
*.h5
# ============================
# OS
# ============================
.DS_Store
Thumbs.db
desktop.ini
# ============================
# Ollama
# ============================
Modelfile

160
README.md Normal file
View file

@ -0,0 +1,160 @@
# Superviseur Bot (Bêta)
Bot Discord IA d'assistance et de modération avancée, utilisant **Ollama** pour l'inférence LLM locale.
## Fonctionnalités
- **IA conversationnelle** : Répond aux questions et exécute des actions sur Discord via JSON
- **Modération silencieuse** : Scan de toxicité de chaque message via LLM en arrière-plan
- **Gestion vocale** : Transcription Whisper en temps réel + détection de mots-clés
- **Mémoire utilisateur** : Historique par utilisateur avec résumé automatique (via LLM)
- **Dispatch d'insights** : Coordination staff avec boutons d'acceptation Discord
- **RGPD** : Purge automatique des données > 30 jours
## Installation
### Prérequis
- Python 3.10+
- **Ollama** installé et en cours d'exécution (https://ollama.com)
- Modèle `gpt-oss:20b` disponible dans Ollama
### Dépendances
```bash
pip install -r requirements.txt
```
### Configuration Ollama
```bash
# Pull le modèle dans Ollama
ollama pull gpt-oss:20b
```
### Configuration
Créer un fichier `.env` à la racine du dossier `beta/` :
```bash
# Discord
DISCORD_TOKEN=your_discord_bot_token
GUILD_ID=your_guild_id
# LLM (Ollama)
OLLAMA_MODEL=gpt-oss:20b
OLLAMA_BASE_URL=http://localhost:11434
# Staff
ADMIN_IDS=123456789,987654321
PRIMARY_ASSETS_IDS=111222333,444555666
# Channel d'introspection
INTROSPECTION_CHANNEL_ID=123456789
```
## Utilisation
```bash
cd beta
python main.py
```
Le bot va :
1. Se connecter à Ollama
2. Initialiser tous les composants (mémoire, modération, vocal)
3. Se connecter à Discord
4. Traiter les messages
## Architecture
```
beta/
├── main.py # Point d'entrée
├── core/
│ ├── bot.py # Classe Beta (événements, orchestration)
│ ├── llm.py # Gestionnaire LLM (Ollama API)
│ ├── action_executor.py # Routage des actions IA → Discord
│ ├── action_router.py # (legacy, redirige vers action_executor)
│ ├── response_handler.py # Nettoyage, parsing JSON, envoi réponses
│ ├── log_parser.py # Parsing des logs d'actions
│ ├── content.py # Préparation du contenu pour le LLM
│ ├── context_builder.py # Construction du contexte serveur
│ ├── dispatcher.py # Dispatch d'insights vers staff
│ ├── voice.py # Gestion vocale + Whisper
│ ├── task_manager.py # Tâches de fond
│ ├── messaging.py # Envoi de messages
│ └── permissions.py # Whitelist et permissions
├── brain/
│ ├── memory.py # Mémoire utilisateur (wrapper)
│ ├── memoire.py # Mémoire utilisateur (implémentation, JSON/Redis + cache)
│ ├── moderation.py # Modération IA + SQLite
│ └── infos_serveurs.py # Infos serveur
├── commandes/
│ ├── security/ # kick, ban, mute, warn, purge...
│ ├── salons/ # Créer, supprimer, modifier salons
│ ├── roles/ # Créer, supprimer, modifier rôles
│ ├── categories/ # Créer, supprimer, modifier catégories
│ └── autres/ # Config, status, ping, etc.
├── data/ # Logs, DB SQLite
└── memoires/ # Historique utilisateur (JSON, par guild)
```
## Actions IA
Le bot peut exécuter les actions suivantes via LLM :
- `CREATE_CHANNEL`, `DELETE_CHANNEL`, `MODIFY_CHANNEL`, `RENAME_CHANNEL`, `MOVE_CHANNEL`
- `CREATE_ROLE`, `DELETE_ROLE`, `ADD_ROLE_TO_USER`, `REMOVE_ROLE_FROM_USER`
- `CREATE_CATEGORY`, `DELETE_CATEGORY`, `MODIFY_CATEGORY`, `RENAME_CATEGORY`
- `KICK`, `BAN`, `UNBAN`, `MUTE`, `UNMUTE`, `TIMEOUT`, `WARN`, `PURGE`
- `JOIN_VOICE`, `LEAVE_VOICE`
- `ALERT`, `INSIGHT`, `FORGET_USER`, `READ_LOGS`
- `SEND_MESSAGE`, `HELP`, `STATUS`
## Variables d'environnement
| Variable | Description | Défaut |
|----------|-------------|--------|
| `DISCORD_TOKEN` | Token du bot Discord | *requis* |
| `GUILD_ID` | ID du serveur principal | *requis* |
| `OLLAMA_MODEL` | Nom du modèle Ollama | `gpt-oss:20b` |
| `OLLAMA_BASE_URL` | URL de l'API Ollama | `http://localhost:11434` |
| `ADMIN_IDS` | IDs des administrateurs (virgule) | — |
| `PRIMARY_ASSETS_IDS` | IDs du staff (virgule) | — |
| `INTROSPECTION_CHANNEL_ID` | Channel de logs internes | — |
| `TIMEZONE` | Fuseau horaire | `Europe/Paris` |
| `SUPERVISEUR_MAX_CONCURRENT` | Requêtes LLM simultanées max | `4` |
| `SUPERVISEUR_MEM_DIR` | Répertoire des mémoires | `memoires` |
| `SUPERVISEUR_MAX_RECENT` | Messages avant résumé | `20` |
| `SUPERVISEUR_REDIS_URL` | URL Redis (optionnel) | — |
| `LOG_LEVEL` | Niveau de log | `INFO` |
## Tests
```bash
# Tests unitaires
pytest test_bot.py -v
# Tests mémoire
python -m brain.memoire
```
## Monitoring
Le bot inclut des métriques intégrées :
- Nombre de requêtes LLM (succès/échecs)
- Temps de réponse moyen
- Métriques mémoire (hits/misses cache, lectures/écritures)
## Sécurité
- **Local uniquement** : Pas d'appels réseau externes
- **RGPD** : Purge automatique des données > 30 jours
- **Permissions** : Vérification admin pour les actions destructrices
- **Whitelist** : Système de niveaux de permissions
## Support
1. Vérifier les logs dans `data/bot.log`
2. Activer le mode debug : `LOG_LEVEL=DEBUG`
3. Vérifier qu'Ollama est actif : `ollama list`

View file

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

215
beta.py
View file

@ -1,215 +0,0 @@
import os
from dotenv import load_dotenv
import discord
import importlib
from superviseur import Superviseur
from commandes.autres.config import config
import threading
import asyncio
# --- Configuration et Chargement des Variables d'Environnement ---
load_dotenv()
DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
try:
GUILD_ID = int(os.getenv('GUILD_ID'))
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("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)
OLLAMA_MODEL_HEAVY = os.getenv("OLLAMA_MODEL_HEAVY", "gpt-oss:120b")
# Modèle "Fast" pour le monitoring et les insights (Irrelevant)
OLLAMA_MODEL_FAST = os.getenv("OLLAMA_MODEL_FAST", "deepseek-r1:7b")
# LE MODELE PAR DEFAUT (Maintenu pour compatibilité si nécessaire)
OLLAMA_MODEL = OLLAMA_MODEL_HEAVY
# LE MODELE OLLAMA UTILISE POUR LA MODÉRATION AUTOMATIQUE !
OLLAMA_MODERATION_MODEL = os.getenv("OLLAMA_MODERATION_MODEL", "deepseek-r1:7b")
OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY")
try:
OLLAMA_TIMEOUT = int(os.getenv("OLLAMA_TIMEOUT", 600))
except (ValueError, TypeError):
print("Avertissement: OLLAMA_TIMEOUT n'est pas un entier valide. Utilisation de la valeur par défaut (600s).")
OLLAMA_TIMEOUT = 600
try:
OLLAMA_TEMPERATURE = float(os.getenv("OLLAMA_TEMPERATURE", 0.4))
except (ValueError, TypeError):
print("Avertissement: OLLAMA_TEMPERATURE n'est pas un nombre valide. Utilisation de la valeur par défaut (0.7).")
OLLAMA_TEMPERATURE = 0.4
# --- PROMPT SYSTÈME CENTRALISÉ ---
# Ceci est l'instruction donnée au modèle LLM pour définir son rôle et les formats de réponse.
# Les actions disponibles dans le prompt DOIVENT correspondre aux fonctions implémentées
# dans superviseur_bot.py.
SYSTEM_PROMPT = (
"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"
"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) :\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()
intents.message_content = True
intents.members = True
intents.voice_states = True
# Optional: faster event loop with uvloop in production (toggle with env USE_UVLOOP=1)
if os.environ.get("USE_UVLOOP", "0") in ("1", "true", "True"):
try:
uvloop = importlib.import_module("uvloop")
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
print("uvloop enabled — using high-performance event loop")
except Exception:
print("uvloop requested but not available; continuing with default event loop")
async def send_hello(bot):
"""Send 'hello' message to the first text channel of the guild."""
guild = bot.get_guild(bot.guild_id)
if guild:
channel = next((ch for ch in guild.text_channels if ch.permissions_for(guild.me).send_messages), None)
if channel:
await channel.send("hello")
else:
print("No text channel found where I can send messages.")
else:
print("Guild not found.")
def maybe_start_metrics(bot):
"""Start metrics server if METRICS_ENABLED is set."""
if os.getenv("METRICS_ENABLED", "0") in ("1", "true", "True"):
app = FastAPI()
prometheus_client.start_http_server(port=8000, addr="127.0.0.1")
@app.get("/metrics")
def get_metrics():
return bot.get_metrics()
config = Config(app=app, host="127.0.0.1", port=8000, log_level="info")
server = Server(config)
def run_server():
asyncio.run(server.serve())
thread.start()
print("Metrics server started on http://127.0.0.1:8000")
def check_ollama_connection(url: str) -> bool:
"""Check if Ollama is accessible."""
import urllib.request
import urllib.error
# 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", "")
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)
print(f"✅ Connexion Ollama établie avec succès !")
return True
except Exception as e:
print(f"⚠️ Attention: Impossible de joindre Ollama ({e}).")
print("Le bot va démarrer mais l'IA risque de ne pas répondre.")
return False
def load_libopus():
"""Load the Opus library for voice support on Linux."""
if not discord.opus.is_loaded():
import ctypes.util
# 1. Search via system utility
found_path = ctypes.util.find_library('opus')
# 2. Hardcoded common Linux paths
opus_libs = [
'libopus.so.0',
'libopus.so',
'/usr/lib/x86_64-linux-gnu/libopus.so.0',
'/usr/lib/libopus.so.0',
'/usr/local/lib/libopus.so'
]
if found_path:
opus_libs.insert(0, found_path)
for lib in opus_libs:
try:
discord.opus.load_opus(lib)
print(f"✅ Bibliothèque Opus chargée : {lib}")
return True
except Exception:
continue
print("⚠️ Attention: Impossible de localiser libopus sur votre système.")
return False
return True
if __name__ == "__main__":
if not DISCORD_TOKEN:
print("Erreur: DISCORD_TOKEN n'est pas défini. Veuillez vérifier votre fichier .env.")
else:
# Vérification de la connexion Ollama
check_ollama_connection(OLLAMA_API_URL)
# Chargement de libopus pour la voix
load_libopus()
# Initialisation du bot avec les configurations
bot = Superviseur(
guild_id=GUILD_ID,
ollama_api_url=OLLAMA_API_URL,
ollama_model=OLLAMA_MODEL,
ollama_timeout=OLLAMA_TIMEOUT,
system_prompt=SYSTEM_PROMPT,
command_prefix="!",
intents=intents,
ollama_temperature=OLLAMA_TEMPERATURE,
# Configuration multi-modèles pour Bêta
ollama_api_key=OLLAMA_API_KEY,
ollama_moderation_model=OLLAMA_MODERATION_MODEL,
ollama_model_fast=OLLAMA_MODEL_FAST
)
# Lancement du bot
bot.run(DISCORD_TOKEN)

View file

@ -1,165 +0,0 @@
import os
from dotenv import load_dotenv
import discord
import importlib
from superviseur import Superviseur
from commandes.autres.config import config
import threading
import asyncio
# --- Configuration et Chargement des Variables d'Environnement ---
load_dotenv()
DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
try:
GUILD_ID = int(os.getenv('GUILD_ID'))
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"
# LE MODELE OLLAMA UTILISE POUR LES CONVERSATIONS !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gemma3:12b") # Laisser ces lignes intactes !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gpt-oss:20b") # Laisser ces lignes intactes !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "deepseek-r1:7b") # Laisser ces lignes intactes !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "nemotron-3-nano:30b") # Laisser ces lignes intactes !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gemini-3-pro-preview") # Laisser ces lignes intactes !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gpt-oss:20b-cloud") # Laisser ces lignes intactes !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "kimi-k2:1t-cloud") # Laisser ces lignes intactes !OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "deepseek-r1:7b") # Laisser ces lignes intactes !
#OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "SimonPu/gpt-oss:20b_Q4_K_M")
# LE MODELE OLLAMA UTILISE POUR LA MODÉRATION AUTOMATIQUE !
#OLLAMA_MODERATION_MODEL = os.getenv("OLLAMA_MODERATION_MODEL", "kimi-k2:1t-cloud") # Modèle pour la modération automatique
#OLLAMA_MODERATION_MODEL = os.getenv("OLLAMA_MODERATION_MODEL", "nemotron-3-nano:30b") # Modèle pour la modération automatique
OLLAMA_MODERATION_MODEL = os.getenv("OLLAMA_MODERATION_MODEL", "deepseek-r1:7b") # Modèle pour la modération automatique
#OLLAMA_MODERATION_MODEL = os.getenv("OLLAMA_MODERATION_MODEL", "gpt-oss:20b") # Modèle pour la modération automatique
OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY")
try:
OLLAMA_TIMEOUT = int(os.getenv("OLLAMA_TIMEOUT", 600))
except (ValueError, TypeError):
print("Avertissement: OLLAMA_TIMEOUT n'est pas un entier valide. Utilisation de la valeur par défaut (600s).")
OLLAMA_TIMEOUT = 600
try:
OLLAMA_TEMPERATURE = float(os.getenv("OLLAMA_TEMPERATURE", 0.2))
except (ValueError, TypeError):
print("Avertissement: OLLAMA_TEMPERATURE n'est pas un nombre valide. Utilisation de la valeur par défaut (0.7).")
OLLAMA_TEMPERATURE = 0.2
# --- PROMPT SYSTÈME CENTRALISÉ ---
# Ceci est l'instruction donnée au modèle LLM pour définir son rôle et les formats de réponse.
# Les actions disponibles dans le prompt DOIVENT correspondre aux fonctions implémentées
# dans superviseur_bot.py.
SYSTEM_PROMPT = (
"Vous êtes le Superviseur, un assistant de modération hautement efficace sur un serveur Discord."
"Votre rôle est d'analyser la demande de l'utilisateur."
"Il y a deux types d'utilisateurs : les utilisateurs réguliers et les modérateurs."
"Les informations système suivantes te sont toujours fournies avant la conversation : une ligne de la forme 'Permissions de l'utilisateur: oui|non, Niveau whitelist: <valeur>'."
"Si cette ligne indique 'oui', ALORS tu dois considérer que l'utilisateur est un modérateur autorisé à utiliser les actions JSON ci-dessous, même si la liste des rôles du serveur ne permet pas de le déduire."
"Si cette ligne indique 'non', tu ne dois JAMAIS exécuter d'actions de modération (KICK, BAN, etc.) et tu dois uniquement répondre en langage naturel avec l'action 'NONE'."
"Si l'utilisateur est un modérateur, tu peux répondre en langage naturel pour assister à des tâches générales (comme rédiger des lettres, fournir des conseils, répondre à des questions) ou effectuer des actions de modération et de gestion."
"Si l'utilisateur n'est PAS un modérateur, tu ne peux répondre qu'en langage naturel."
"--- Format de Réponse pour Langage Naturel (NON-ACTION) ---\n"
"Répondez avec : `{\"action\": \"NONE\", \"response\": \"[votre réponse en français ici]\"}`\n\n"
"--- Actions de Modération & Gestion (si l'utilisateur est un modérateur) ---\n"
"Utilisez ces formats jsons, vous pouvez combiner plusieurs jsons à la suite: \n"
"- Expulsion : `{\"action\": \"KICK\", \"users\": [{\"target_user_mention\": \"[@mention ou ID utilisateur/bot]\"}]}`\n"
"- Bannir : `{\"action\": \"BAN\", \"users\": [{\"target_user_mention\": \"[@mention ou ID]\"}]}`\n"
"- Débannir : `{\"action\": \"UNBAN\", \"users\": [{\"target_user_mention\": \"[@mention ou ID]\"}]}`\n"
"- Muter : `{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"[@mention ou ID]\", \"duration\": [durée en minutes]}]}`\n"
"- Timeout : `{\"action\": \"TIMEOUT\", \"users\": [{\"target_user_mention\": \"[@mention ou ID]\", \"duration\": [durée en minutes]}]}`\n"
"- Purger : `{\"action\": \"PURGE\", \"purges\": [{\"channel_name\": \"[nom du salon]\", \"amount\": [nombre de messages]}]}`\n"
"- Avertir : `{\"action\": \"WARN\", \"warnings\": [{\"target_user_mention\": \"[@mention ou ID]\", \"reason\": \"[raison]\"}]}`\n"
"- Créer Salon : `{\"action\": \"CREATE_CHANNEL\", \"channels\": [{\"channel_name\": \"[nom du salon]\", \"category_name\": \"[nom de la catégorie]\", \"channel_type\": \"[textuel|vocal]\"}]}`\n"
"- Supprimer Salon : `{\"action\": \"DELETE_CHANNEL\", \"channels\": [{\"channel_name\": \"[nom du salon ou de la catégorie]\"}]}`\n"
"- Modifier Salon : `{\"action\": \"MODIFY_CHANNEL\", \"channels\": [{\"channel_name\": \"[nom actuel]\", \"new_name\": \"[nouveau nom]\", \"new_category_name\": \"[nouvelle catégorie]\", \"topic\": \"[nouveau sujet (pour textuel)]\", \"user_limit\": [nouvelle limite (pour vocal)]}]}` (Omettez les clés non pertinentes/non demandées pour chaque salon)\n"
"- Déplacer Salon : `{\"action\": \"MOVE_CHANNEL\", \"channels\": [{\"channel_name\": \"[nom]\", \"new_category_name\": \"[nouvelle catégorie]\"}]}`\n"
"- Renommer Salon : `{\"action\": \"RENAME_CHANNEL\", \"channels\": [{\"channel_name\": \"[nom actuel]\", \"new_name\": \"[nouveau nom]\"}]}`\n"
"- Créer Rôle : `{\"action\": \"CREATE_ROLE\", \"roles\": [{\"role_name\": \"[nom]\", \"color\": \"[couleur hex, e.g. #ff0000]\", \"permissions\": \"[liste de permissions séparées par virgule, e.g. kick_members, ban_members]\"}]}`\n"
"- Supprimer Rôle : `{\"action\": \"DELETE_ROLE\", \"roles\": [{\"role_name\": \"[nom]\"}]}`\n"
"- Modifier Rôle :r `{\"action\": \"MODIFY_ROLE\", \"roles\": [{\"role_name\": \"[nom actuel]\", \"new_name\": \"[nouveau nom]\", \"color\": \"[nouvelle couleur hex]\", \"permissions\": \"[nouvelles permissions]\"}]}` (Omettez les clés non pertinentes pour chaque rôle)\n"
"- Renommer Rôle : `{\"action\": \"RENAME_ROLE\", \"roles\": [{\"role_name\": \"[nom actuel]\", \"new_name\": \"[nouveau nom]\"}]}`\n"
"- Déplacer Rôle : `{\"action\": \"MOVE_ROLE\", \"roles\": [{\"role_name\": \"[nom]\", \"position\": [nouvelle position]}]}`\n"
"- Ajouter Rôle à Utilisateur : `{\"action\": \"ADD_ROLE_TO_USER\", \"role_assignments\": [{\"target_user_mention\": \"[@mention ou ID]\", \"role_name\": \"[nom du rôle]\"}]}`\n"
"- Retirer Rôle d'Utilisateur : `{\"action\": \"REMOVE_ROLE_FROM_USER\", \"role_removals\": [{\"target_user_mention\": \"[@mention ou ID]\", \"role_name\": \"[nom du rôle]\"}]}`\n"
"- Créer Catégorie : `{\"action\": \"CREATE_CATEGORY\", \"categories\": [{\"category_name\": \"[nom]\"}]}`\n"
"- Supprimer Catégorie : `{\"action\": \"DELETE_CATEGORY\", \"categories\": [{\"category_name\": \"[nom]\"}]}`\n"
"- Modifier Catégorie : `{\"action\": \"MODIFY_CATEGORY\", \"categories\": [{\"category_name\": \"[nom actuel]\", \"new_name\": \"[nouveau nom]\"}]}\n"
"- Déplacer Catégorie : `{\"action\": \"MOVE_CATEGORY\", \"categories\": [{\"category_name\": \"[nom]\", \"position\": [nouvelle position]}]}`\n"
"- Renommer Catégorie : `{\"action\": \"RENAME_CATEGORY\", \"categories\": [{\"category_name\": \"[nom actuel]\", \"new_name\": \"[nouveau nom]\"}]}`\n"
"- Ping : `{\"action\": \"PING\"}`\n"
"- Définir Salon de Bienvenue : `{\"action\": \"SET_WELCOME_CHANNEL\", \"channel_name\": \"[nom]\"}`\n"
"- Définir Salon d'Au Revoir : `{\"action\": \"SET_GOODBYE_CHANNEL\", \"channel_name\": \"[nom]\"}`\n"
"- Définir Rôle Mute : `{\"action\": \"SET_MUTE_ROLE\", \"role_name\": \"[nom]\"}`\n"
"- Envoyer Message : `{\"action\": \"SEND_MESSAGE\", \"channel_name\": \"[nom du salon (facultatif)]\", \"dm_user\": \"[@mention ou ID (facultatif)]\", \"message\": \"[contenu]\"}` (au moins un des deux: channel_name ou dm_user)\n\n"
)
# --- Intents Discord ---
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
# Optional: faster event loop with uvloop in production (toggle with env USE_UVLOOP=1)
if os.environ.get("USE_UVLOOP", "0") in ("1", "true", "True"):
try:
uvloop = importlib.import_module("uvloop")
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
print("uvloop enabled — using high-performance event loop")
except Exception:
print("uvloop requested but not available; continuing with default event loop")
async def send_hello(bot):
"""Send 'hello' message to the first text channel of the guild."""
guild = bot.get_guild(bot.guild_id)
if guild:
channel = next((ch for ch in guild.text_channels if ch.permissions_for(guild.me).send_messages), None)
if channel:
await channel.send("hello")
else:
print("No text channel found where I can send messages.")
else:
print("Guild not found.")
def maybe_start_metrics(bot):
"""Start metrics server if METRICS_ENABLED is set."""
if os.getenv("METRICS_ENABLED", "0") in ("1", "true", "True"):
app = FastAPI()
prometheus_client.start_http_server(port=8000, addr="127.0.0.1")
@app.get("/metrics")
def get_metrics():
return bot.get_metrics()
config = Config(app=app, host="127.0.0.1", port=8000, log_level="info")
server = Server(config)
def run_server():
asyncio.run(server.serve())
thread = threading.Thread(target=run_server, daemon=True)
thread.start()
print("Metrics server started on http://127.0.0.1:8000")
if __name__ == "__main__":
if not DISCORD_TOKEN:
print("Erreur: DISCORD_TOKEN n'est pas défini. Veuillez vérifier votre fichier .env.")
else:
# Initialisation du bot avec les configurations
bot = Superviseur(
guild_id=GUILD_ID,
ollama_api_url=OLLAMA_API_URL,
ollama_model=OLLAMA_MODEL,
ollama_timeout=OLLAMA_TIMEOUT,
system_prompt=SYSTEM_PROMPT,
command_prefix="!",
intents=intents,
ollama_temperature=OLLAMA_TEMPERATURE,
# Correction : Ajout des variables de modération
ollama_api_key=OLLAMA_API_KEY,
ollama_moderation_model=OLLAMA_MODERATION_MODEL
)
# Lancement du bot
bot.run(DISCORD_TOKEN)

View file

@ -253,17 +253,72 @@ async def save_user_memory_async(user_id: int | str, data: Dict[str, Any], guild
logger.debug(f"Mémoire marquée dirty pour utilisateur {user_id} (guild {guild_id})")
# Référence globale au LLM manager (set par le bot au démarrage)
_llm_manager = None
def set_llm_manager(llm_manager) -> None:
"""Enregistre le LLM manager pour les résumés."""
global _llm_manager
_llm_manager = llm_manager
async def summarize_messages(messages: List[Dict[str, Any]]) -> str:
"""
Fonction de résumé améliorée. Utilise une extraction de mots-clés et de phrases clés
pour un résumé plus intelligent. Placeholder pour intégration IA future.
Cette fonction est `async` pour faciliter un remplacement par un appel réseau/IA.
Résume une liste de messages via le LLM.
Fallback sur l'extraction de mots-clés si le LLM n'est pas disponible.
"""
if not messages:
return ""
# Extraire tous les textes utilisateur et bot
# Construire le transcript à résumer
transcript_lines = []
for m in messages:
um = m.get("user_message", "").strip()
bm = m.get("bot_message", "").strip()
if um:
transcript_lines.append(f"Utilisateur: {um}")
if bm:
transcript_lines.append(f"Bot: {bm}")
transcript = "\n".join(transcript_lines)
# Tronquer le transcript pour éviter les appels trop longs
if len(transcript) > 4000:
transcript = transcript[-4000:]
# Essayer d'utiliser le LLM pour un vrai résumé
if _llm_manager is not None:
try:
prompt = (
"Tu es un assistant spécialisé dans la synthèse de conversations.\n"
"Résume les échanges suivants de manière concise et factuelle.\n"
"Extrais les sujets abordés, les décisions prises, et les informations importantes.\n"
"Le résumé ne doit PAS dépasser 500 caractères.\n\n"
f"CONVERSATION À RÉSUMER :\n{transcript}\n\n"
"RÉSUMÉ :"
)
payload = _llm_manager.build_payload(
prompt=prompt,
system_prompt="Tu es un assistant de synthèse. Sois concis et factuel.",
force_json=False
)
summary = await _llm_manager.call_llama(payload)
if summary and len(summary.strip()) > 10:
# Nettoyer le résumé
summary = summary.strip()
if len(summary) > 1500:
summary = summary[:1497] + "..."
return summary
except Exception as e:
logger.warning(f"Erreur résumé LLM, fallback extractif: {e}")
# Fallback : extraction de mots-clés et phrases clés
return _extractive_summary(messages)
def _extractive_summary(messages: List[Dict[str, Any]]) -> str:
"""Résumé extractif basé sur les mots-clés et phrases clés (fallback)."""
all_texts = []
for m in messages:
um = m.get("user_message", "")
@ -272,7 +327,7 @@ async def summarize_messages(messages: List[Dict[str, Any]]) -> str:
combined_text = " ".join(all_texts)
# Extraction simple de mots-clés (mots fréquents, >3 chars)
# Extraction de mots-clés (mots fréquents, >3 chars)
words = re.findall(r'\b\w{4,}\b', combined_text.lower())
word_freq = {}
for word in words:
@ -282,28 +337,25 @@ async def summarize_messages(messages: List[Dict[str, Any]]) -> str:
top_keywords = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:10]
keywords_str = ", ".join([kw for kw, _ in top_keywords])
# Extraire phrases clés (premières phrases des messages importants)
# Extraire phrases clés
key_phrases = []
for m in messages:
um = m.get("user_message", "").strip()
bm = m.get("bot_message", "").strip()
if um:
sentences = re.split(r'[.!?]', um)[:1] # première phrase
sentences = re.split(r'[.!?]', um)[:1]
key_phrases.extend(sentences)
if bm:
sentences = re.split(r'[.!?]', bm)[:1]
key_phrases.extend(sentences)
phrases_str = " | ".join(key_phrases[:5]) # limiter à 5 phrases
phrases_str = " | ".join(key_phrases[:5])
# Construire le résumé
summary = f"Mots-clés: {keywords_str}\nPhrases clés: {phrases_str}"
# Tronquer si trop long
if len(summary) > 1500:
summary = summary[:1497] + "..."
await asyncio.sleep(0) # point d'await pour remplacer par un vrai appel
return summary
@ -401,10 +453,10 @@ async def build_context_for_model(
parts.append(f"Utilisateur: {um}")
parts.append(f"Bot: {bm}")
# Contrainte : ne pas renvoyer un prompt de plus de ~6000 caractères (ajustable)
# Contrainte : ne pas renvoyer un prompt de plus de ~3000 caractères (ajustable)
joined = "\n".join(parts)
if len(joined) > 6000:
return joined[-6000:]
if len(joined) > 3000:
return joined[-3000:]
return joined
@ -479,12 +531,32 @@ def list_memory_files() -> List[str]:
return files
def delete_user_memory(user_id: int | str) -> None:
path = _user_filepath(user_id)
def delete_user_memory(user_id: int | str, guild_id: str | None = None) -> None:
"""Supprime la mémoire d'un utilisateur, optionnellement pour un guild spécifique."""
path = _user_filepath(user_id, guild_id)
try:
os.remove(path)
except FileNotFoundError:
pass
# Si aucun guild_id spécifique, supprimer aussi tous les fichiers de mémoire pour cet utilisateur
if guild_id is None:
_ensure_memory_dir()
user_str = str(user_id)
for root, dirs, files in os.walk(MEMORY_DIR):
for name in files:
if name == f"{user_str}.json" or name == f"{user_id}.json":
full_path = os.path.join(root, name)
if full_path != path: # Éviter double suppression
try:
os.remove(full_path)
except FileNotFoundError:
pass
# Nettoyer le cache
key_no_guild = f"_{user_id}" if guild_id is None else f"{guild_id}_{user_id}"
memory_cache.pop(key_no_guild, None)
_dirty.pop(key_no_guild, None)
# Fonction pour obtenir les métriques

16
brain/memory.py Normal file
View file

@ -0,0 +1,16 @@
"""User memory management - JSON/Redis with in-memory cache."""
# Renamed from memoire.py for consistency (English naming)
from .memoire import ( # noqa: F401
add_interaction,
build_context_for_model,
flush_all,
MEMORY_DIR,
set_llm_manager,
delete_user_memory,
purge_old_memories,
get_metrics,
_extractive_summary,
load_user_memory_async,
save_user_memory_async,
)

80
brain/moderation.py Normal file
View file

@ -0,0 +1,80 @@
import sqlite3
import datetime
import logging
from typing import Optional
logger = logging.getLogger('Beta')
DB_PATH = 'data/mod.db'
def init_db():
"""Initialise la base de données de modération."""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS infractions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
username TEXT,
guild_id TEXT,
content TEXT,
score REAL,
reason TEXT,
timestamp TEXT
)
''')
conn.commit()
conn.close()
logger.info("✅ Base de données de modération initialisée.")
def log_infraction(user_id: str, username: str, guild_id: str, content: str, score: float, reason: str):
"""Enregistre une note de modération en base de données."""
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
timestamp = datetime.datetime.now().isoformat()
cursor.execute('''
INSERT INTO infractions (user_id, username, guild_id, content, score, reason, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (user_id, username, guild_id, content, score, reason, timestamp))
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Erreur lors de l'enregistrement de l'infraction: {e}")
async def scan_message_toxicity(llm_manager, content: str) -> Optional[dict]:
"""
Analyse le contenu d'un message via l'IA pour obtenir une note de toxicité.
Retourne un dict {score, reason} ou None.
"""
if not content or len(content.strip()) < 3:
return None
prompt = (
"Analyse la toxicité de ce message Discord. "
"Réponds UNIQUEMENT au format JSON suivant : "
"{\"score\": <0.0 à 1.0>, \"reason\": \"brève explication\"}\n"
"Un score de 0.0 est neutre/positif, 1.0 est extrêmement dégradant/toxique.\n"
f"Message : \"{content}\""
)
payload = {
"prompt": prompt,
"system_prompt": "Tu es un expert en modération. Sois objectif et juste.",
"temperature": 0.1,
"max_tokens": 300,
"force_json": True,
"stop": ["User:", "Assistant:"]
}
try:
# On utilise une version simplifiée de l'appel pour gagner du temps
raw_resp = await llm_manager.call_llama(payload)
import json
data = json.loads(llm_manager.extract_json_actions(raw_resp) or "{}")
if 'score' in data:
return data
except Exception as e:
logger.error(f"Erreur scan toxicité: {e}")
return None

Binary file not shown.

Binary file not shown.

View file

@ -4,7 +4,7 @@ from datetime import datetime
from .config import config
class ActionLogger:
def __init__(self, log_file: str = 'actions.log'):
def __init__(self, log_file: str = 'data/actions.log'):
self.log_file = log_file
self.logger = logging.getLogger('ActionLogger')
self.logger.setLevel(logging.INFO)
@ -41,13 +41,16 @@ class ActionLogger:
def log_message(self, level: str, message: str):
"""Log a general message."""
if level.casefold() == 'info':
if not level or not message:
return
level_lower = level.casefold()
if level_lower == 'info':
self.logger.info(message)
elif level.casefold() == 'warning':
elif level_lower == 'warning':
self.logger.warning(message)
elif level.casefold() == 'error':
elif level_lower == 'error':
self.logger.error(message)
elif level.casefold() == 'debug':
elif level_lower == 'debug':
self.logger.debug(message)
# Global logger instance

View file

@ -3,6 +3,9 @@ from .config import config
async def execute(bot, params, message):
channel_name = params.get('channel_name')
if not channel_name:
await message.channel.send("Veuillez spécifier un nom de salon.")
return
guild = message.guild
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
if not channel:

View file

@ -3,6 +3,9 @@ from .config import config
async def execute(bot, params, message):
channel_name = params.get('channel_name')
if not channel_name:
await message.channel.send("Veuillez spécifier un nom de salon.")
return
guild = message.guild
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
if not channel:

View file

@ -9,6 +9,9 @@ async def execute(bot, params, message):
created_count = 0
for category_params in categories:
category_name = category_params.get('category_name')
if not category_name:
await message.channel.send("Nom de catégorie requis pour la création.")
continue
try:
category = await guild.create_category(category_name)
created_count += 1

View file

@ -1,5 +1,22 @@
import discord
async def _find_category(guild, category_params):
"""Find a category by ID first, then by name (case-insensitive)."""
category_id = category_params.get('category_id')
if category_id:
try:
cat = guild.get_channel(int(category_id))
if cat and isinstance(cat, discord.CategoryChannel):
return cat
except (ValueError, TypeError):
pass
category_name = category_params.get('category_name')
if not category_name:
return None
return discord.utils.find(lambda cat: cat.name.casefold() == category_name.casefold(), guild.categories)
async def execute(bot, params, message):
categories = params.get('categories', [])
if not categories:
@ -10,14 +27,17 @@ async def execute(bot, params, message):
for category_params in categories:
category_name = category_params.get('category_name')
position = category_params.get('position')
category = discord.utils.find(lambda cat: cat.name.casefold() == category_name.casefold(), guild.categories)
category = await _find_category(guild, category_params)
if not category:
await message.channel.send(f"Catégorie '{category_name}' introuvable.")
label = category_name or category_params.get('category_id', 'inconnue')
await message.channel.send(f"Catégorie '{label}' introuvable.")
continue
try:
await category.edit(position=position)
moved_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour déplacer la catégorie '{category_name}'.")
label = category_name or category_params.get('category_id', 'inconnue')
await message.channel.send(f"Je n'ai pas les permissions pour déplacer la catégorie '{label}'.")
if moved_count > 0:
await message.channel.send(f"{moved_count} catégorie(s) déplacée(s).")
await message.channel.send(f"{moved_count} catégorie(s) déplacée(s).")

View file

@ -1,5 +1,22 @@
import discord
async def _find_category(guild, category_params):
"""Find a category by ID first, then by name (case-insensitive)."""
category_id = category_params.get('category_id')
if category_id:
try:
cat = guild.get_channel(int(category_id))
if cat and isinstance(cat, discord.CategoryChannel):
return cat
except (ValueError, TypeError):
pass
category_name = category_params.get('category_name')
if not category_name:
return None
return discord.utils.find(lambda cat: cat.name.casefold() == category_name.casefold(), guild.categories)
async def execute(bot, params, message):
categories = params.get('categories', [])
if not categories:
@ -10,14 +27,17 @@ async def execute(bot, params, message):
for category_params in categories:
category_name = category_params.get('category_name')
new_name = category_params.get('new_name')
category = discord.utils.find(lambda cat: cat.name.casefold() == category_name.casefold(), guild.categories)
category = await _find_category(guild, category_params)
if not category:
await message.channel.send(f"Catégorie '{category_name}' introuvable.")
label = category_name or category_params.get('category_id', 'inconnue')
await message.channel.send(f"Catégorie '{label}' introuvable.")
continue
try:
await category.edit(name=new_name)
renamed_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour renommer la catégorie '{category_name}'.")
label = category_name or category_params.get('category_id', 'inconnue')
await message.channel.send(f"Je n'ai pas les permissions pour renommer la catégorie '{label}'.")
if renamed_count > 0:
await message.channel.send(f"{renamed_count} catégorie(s) renommée(s).")
await message.channel.send(f"{renamed_count} catégorie(s) renommée(s).")

View file

@ -1,15 +1,52 @@
import discord
import re
def strip_emoji(text):
"""Remove emoji characters for fuzzy matching."""
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF"
u"\U0001F1E0-\U0001F1FF\U00002702-\U000027B0\U000024C2-\U0001F251"
u"\U0000FE00-\U0000FE0F\U0000200D\U0000E0100-\U0000E01EFF"
u"\u20E3\u00A9\u00AE\u2122"
"]+", flags=re.UNICODE)
return emoji_pattern.sub('', text).strip()
async def find_category(guild, category_name_or_id):
"""Find a category by ID or name (exact then fuzzy)."""
# 1. Try by ID
try:
cat_id = int(category_name_or_id)
cat = guild.get_channel(cat_id)
if cat and isinstance(cat, discord.CategoryChannel):
return cat
except (ValueError, TypeError):
pass
# 2. Exact name
for cat in guild.categories:
if cat.name.casefold() == category_name_or_id.casefold():
return cat
# 3. Fuzzy (strip emoji)
clean_search = strip_emoji(category_name_or_id).lower().strip()
for cat in guild.categories:
clean_name = strip_emoji(cat.name).lower().strip()
if clean_name == clean_search or (clean_search and clean_search in clean_name):
return cat
return None
async def execute(bot, params, message):
categories = params.get('categories', [])
if not categories:
# Fallback for single category
categories = [params]
guild = message.guild
deleted_count = 0
for category_params in categories:
category_name = category_params.get('category_name')
category = discord.utils.find(lambda cat: cat.name.casefold() == category_name.casefold(), guild.categories)
category_name = category_params.get('category_name') or category_params.get('category_id')
if not category_name:
continue
category = await find_category(guild, category_name)
if not category:
await message.channel.send(f"Catégorie '{category_name}' introuvable.")
continue
@ -19,4 +56,4 @@ async def execute(bot, params, message):
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour supprimer la catégorie '{category_name}'.")
if deleted_count > 0:
await message.channel.send(f"{deleted_count} catégorie(s) supprimée(s).")
await message.channel.send(f"{deleted_count} catégorie(s) supprimée(s).")

View file

@ -10,9 +10,22 @@ async def execute(bot, params, message):
for assignment in role_assignments:
target_mention = assignment.get('target_user_mention')
role_name = assignment.get('role_name')
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), message.guild.roles)
# Try by role_id first if available
role = None
role_id = assignment.get('role_id')
if role_id:
try:
role = message.guild.get_role(int(role_id))
except (ValueError, TypeError):
pass
if not role and role_name:
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), message.guild.roles)
if not role:
await message.channel.send(f"Rôle '{role_name}' introuvable.")
label = role_name or assignment.get('role_id', 'inconnu')
await message.channel.send(f"Rôle '{label}' introuvable.")
continue
# Check for bulk assignment

View file

@ -9,6 +9,9 @@ async def execute(bot, params, message):
created_count = 0
for role_params in roles:
role_name = role_params.get('role_name')
if not role_name:
await message.channel.send("Nom de rôle requis pour la création.")
continue
color = role_params.get('color', '#ffffff')
permissions_str = role_params.get('permissions', '')
# Parse color

View file

@ -1,5 +1,22 @@
import discord
async def _find_role(guild, role_params):
"""Find a role by ID first, then by name (case-insensitive)."""
role_id = role_params.get('role_id')
if role_id:
try:
r = guild.get_role(int(role_id))
if r:
return r
except (ValueError, TypeError):
pass
role_name = role_params.get('role_name')
if not role_name:
return None
return discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
async def execute(bot, params, message):
roles = params.get('roles', [])
if not roles:
@ -10,14 +27,17 @@ async def execute(bot, params, message):
for role_params in roles:
role_name = role_params.get('role_name')
position = role_params.get('position')
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
role = await _find_role(guild, role_params)
if not role:
await message.channel.send(f"Rôle '{role_name}' introuvable.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Rôle '{label}' introuvable.")
continue
try:
await role.edit(position=position)
moved_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour déplacer le rôle '{role_name}'.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Je n'ai pas les permissions pour déplacer le rôle '{label}'.")
if moved_count > 0:
await message.channel.send(f"{moved_count} rôle(s) déplacé(s).")
await message.channel.send(f"{moved_count} rôle(s) déplacé(s).")

View file

@ -1,5 +1,22 @@
import discord
async def _find_role(guild, role_params):
"""Find a role by ID first, then by name (case-insensitive)."""
role_id = role_params.get('role_id')
if role_id:
try:
r = guild.get_role(int(role_id))
if r:
return r
except (ValueError, TypeError):
pass
role_name = role_params.get('role_name')
if not role_name:
return None
return discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
async def execute(bot, params, message):
roles = params.get('roles', [])
if not roles:
@ -10,14 +27,17 @@ async def execute(bot, params, message):
for role_params in roles:
role_name = role_params.get('role_name')
new_name = role_params.get('new_name')
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
role = await _find_role(guild, role_params)
if not role:
await message.channel.send(f"Rôle '{role_name}' introuvable.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Rôle '{label}' introuvable.")
continue
try:
await role.edit(name=new_name)
modified_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour modifier le rôle '{role_name}'.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Je n'ai pas les permissions pour modifier le rôle '{label}'.")
if modified_count > 0:
await message.channel.send(f"{modified_count} rôle(s) modifié(s).")
await message.channel.send(f"{modified_count} rôle(s) modifié(s).")

View file

@ -10,13 +10,26 @@ async def execute(bot, params, message):
for removal in role_removals:
target_mention = removal.get('target_user_mention')
role_name = removal.get('role_name')
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), message.guild.roles)
# Try by role_id first if available
role = None
role_id = removal.get('role_id')
if role_id:
try:
role = message.guild.get_role(int(role_id))
except (ValueError, TypeError):
pass
if not role and role_name:
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), message.guild.roles)
if not role:
await message.channel.send(f"Rôle '{role_name}' introuvable.")
label = role_name or removal.get('role_id', 'inconnu')
await message.channel.send(f"Rôle '{label}' introuvable.")
continue
# Check for bulk removal
if target_mention.lower() in ['@everyone', 'everyone', 'all', 'tous']:
if target_mention and target_mention.lower() in ['@everyone', 'everyone', 'all', 'tous']:
total_members = 0
removed_count = 0
await message.channel.send(f"Retrait du rôle '{role.name}' de tous les membres... (Cela peut prendre du temps)")
@ -39,6 +52,10 @@ async def execute(bot, params, message):
await message.channel.send(f"Rôle '{role.name}' retiré à {removed_count} membres (bots exclus).")
continue
if not target_mention:
await message.channel.send("Mention utilisateur requise.")
continue
# Single user removal
if target_mention.startswith('<@') and target_mention.endswith('>'):
user_id = int(target_mention[2:-1])
@ -68,4 +85,4 @@ async def execute(bot, params, message):
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour retirer le rôle {role.name} de {member.mention}.")
if removed_count > 0 and not total_members:
await message.channel.send(f"{removed_count} rôle(s) retiré(s).")
await message.channel.send(f"{removed_count} rôle(s) retiré(s).")

View file

@ -1,5 +1,22 @@
import discord
async def _find_role(guild, role_params):
"""Find a role by ID first, then by name (case-insensitive)."""
role_id = role_params.get('role_id')
if role_id:
try:
r = guild.get_role(int(role_id))
if r:
return r
except (ValueError, TypeError):
pass
role_name = role_params.get('role_name')
if not role_name:
return None
return discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
async def execute(bot, params, message):
roles = params.get('roles', [])
if not roles:
@ -10,14 +27,17 @@ async def execute(bot, params, message):
for role_params in roles:
role_name = role_params.get('role_name')
new_name = role_params.get('new_name')
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
role = await _find_role(guild, role_params)
if not role:
await message.channel.send(f"Rôle '{role_name}' introuvable.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Rôle '{label}' introuvable.")
continue
try:
await role.edit(name=new_name)
renamed_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour renommer le rôle '{role_name}'.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Je n'ai pas les permissions pour renommer le rôle '{label}'.")
if renamed_count > 0:
await message.channel.send(f"{renamed_count} rôle(s) renommé(s).")
await message.channel.send(f"{renamed_count} rôle(s) renommé(s).")

View file

@ -1,5 +1,22 @@
import discord
async def _find_role(guild, role_params):
"""Find a role by ID first, then by name (case-insensitive)."""
role_id = role_params.get('role_id')
if role_id:
try:
r = guild.get_role(int(role_id))
if r:
return r
except (ValueError, TypeError):
pass
role_name = role_params.get('role_name')
if not role_name:
return None
return discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
async def execute(bot, params, message):
roles = params.get('roles', [])
if not roles:
@ -9,14 +26,17 @@ async def execute(bot, params, message):
deleted_count = 0
for role_params in roles:
role_name = role_params.get('role_name')
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), guild.roles)
role = await _find_role(guild, role_params)
if not role:
await message.channel.send(f"Rôle '{role_name}' introuvable.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Rôle '{label}' introuvable.")
continue
try:
await role.delete(reason="Supprimé par le Superviseur")
deleted_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour supprimer le rôle '{role_name}'.")
label = role_name or role_params.get('role_id', 'inconnu')
await message.channel.send(f"Je n'ai pas les permissions pour supprimer le rôle '{label}'.")
if deleted_count > 0:
await message.channel.send(f"{deleted_count} rôle(s) supprimé(s).")
await message.channel.send(f"{deleted_count} rôle(s) supprimé(s).")

View file

@ -9,8 +9,14 @@ async def execute(bot, params, message):
created_count = 0
for channel_params in channels:
channel_name = channel_params.get('channel_name')
if not channel_name:
await message.channel.send("Nom de salon requis pour la création.")
continue
category_name = channel_params.get('category_name')
channel_type = channel_params.get('channel_type', 'textuel')
channel_type = (channel_params.get('channel_type') or 'textuel').casefold()
category = None
if category_name:
# Recherche flexible : trouver une catégorie dont le nom contient le terme recherché
category = discord.utils.find(
@ -25,12 +31,11 @@ async def execute(bot, params, message):
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour créer la catégorie '{category_name}'.")
continue
else:
category = None
try:
if channel_type.casefold() == 'textuel':
if channel_type == 'textuel':
channel = await guild.create_text_channel(channel_name, category=category)
elif channel_type.casefold() == 'vocal':
elif channel_type == 'vocal':
channel = await guild.create_voice_channel(channel_name, category=category)
else:
await message.channel.send(f"Type de salon invalide pour '{channel_name}'. Utilisez 'textuel' ou 'vocal'.")
@ -39,4 +44,4 @@ async def execute(bot, params, message):
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour créer le salon '{channel_name}'.")
if created_count > 0:
await message.channel.send(f"{created_count} salon(s) créé(s).")
await message.channel.send(f"{created_count} salon(s) créé(s).")

View file

@ -1,5 +1,40 @@
import discord
async def _find_channel(guild, channel_params):
"""Find a channel by ID first, then by name (case-insensitive)."""
channel_id = channel_params.get('channel_id')
if channel_id:
try:
ch = guild.get_channel(int(channel_id))
if ch:
return ch
except (ValueError, TypeError):
pass
channel_name = channel_params.get('channel_name')
if not channel_name:
return None
return discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
async def _find_category(guild, category_params):
"""Find a category by ID first, then by name (case-insensitive)."""
# Support both 'category_id' and 'new_category_id'
category_id = category_params.get('new_category_id') or category_params.get('category_id')
if category_id:
try:
cat = guild.get_channel(int(category_id))
if cat and isinstance(cat, discord.CategoryChannel):
return cat
except (ValueError, TypeError):
pass
category_name = category_params.get('new_category_name') or category_params.get('category_name')
if not category_name:
return None
return discord.utils.find(lambda cat: cat.name.casefold() == category_name.casefold(), guild.categories)
async def execute(bot, params, message):
channels = params.get('channels', [])
if not channels:
@ -10,18 +45,26 @@ async def execute(bot, params, message):
for channel_params in channels:
channel_name = channel_params.get('channel_name')
new_category_name = channel_params.get('new_category_name')
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
channel = await _find_channel(guild, channel_params)
if not channel:
await message.channel.send(f"Salon '{channel_name}' introuvable.")
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Salon '{label}' introuvable.")
continue
category = discord.utils.find(lambda cat: cat.name.casefold() == new_category_name.casefold(), guild.categories)
category = await _find_category(guild, channel_params)
if not category:
await message.channel.send(f"Catégorie '{new_category_name}' introuvable.")
continue
has_category_param = any(k in channel_params for k in ['new_category_id', 'new_category_name', 'category_id', 'category_name'])
if has_category_param:
label = new_category_name or channel_params.get('new_category_id') or channel_params.get('category_id', 'inconnue')
await message.channel.send(f"Catégorie '{label}' introuvable.")
continue
try:
await channel.edit(category=category)
moved_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour déplacer le salon '{channel_name}'.")
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Je n'ai pas les permissions pour déplacer le salon '{label}'.")
if moved_count > 0:
await message.channel.send(f"{moved_count} salon(s) déplacé(s).")
await message.channel.send(f"{moved_count} salon(s) déplacé(s).")

View file

@ -1,5 +1,39 @@
import discord
async def _find_channel(guild, channel_params):
"""Find a channel by ID first, then by name (case-insensitive)."""
channel_id = channel_params.get('channel_id')
if channel_id:
try:
ch = guild.get_channel(int(channel_id))
if ch:
return ch
except (ValueError, TypeError):
pass
channel_name = channel_params.get('channel_name')
if not channel_name:
return None
return discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
async def _find_category(guild, category_params):
"""Find a category by ID first, then by name (case-insensitive)."""
category_id = category_params.get('new_category_id') or category_params.get('category_id')
if category_id:
try:
cat = guild.get_channel(int(category_id))
if cat and isinstance(cat, discord.CategoryChannel):
return cat
except (ValueError, TypeError):
pass
category_name = category_params.get('new_category_name') or category_params.get('category_name')
if not category_name:
return None
return discord.utils.find(lambda cat: cat.name.casefold() == category_name.casefold(), guild.categories)
async def execute(bot, params, message):
channels = params.get('channels', [])
if not channels:
@ -9,15 +43,43 @@ async def execute(bot, params, message):
modified_count = 0
for channel_params in channels:
channel_name = channel_params.get('channel_name')
new_name = channel_params.get('new_name')
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
# Support both 'new_name' and 'new_channel_name' variants
new_name = channel_params.get('new_name') or channel_params.get('new_channel_name')
new_category_id = channel_params.get('new_category_id')
new_category_name = channel_params.get('new_category_name')
channel = await _find_channel(guild, channel_params)
if not channel:
await message.channel.send(f"Salon '{channel_name}' introuvable.")
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Salon '{label}' introuvable.")
continue
# Build edit kwargs based on what params are provided
edit_kwargs = {}
if new_name:
edit_kwargs['name'] = new_name
if new_category_id or new_category_name:
category = await _find_category(guild, channel_params)
if category:
edit_kwargs['category'] = category
else:
cat_label = new_category_name or new_category_id or 'inconnue'
await message.channel.send(f"Catégorie '{cat_label}' introuvable.")
continue
if not edit_kwargs:
await message.channel.send("Aucune modification à appliquer (ni nom, ni catégorie spécifiés).")
continue
try:
await channel.edit(name=new_name)
await channel.edit(**edit_kwargs)
modified_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour modifier le salon '{channel_name}'.")
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Je n'ai pas les permissions pour modifier le salon '{label}'.")
except Exception as e:
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Erreur lors de la modification du salon '{label}': {e}")
if modified_count > 0:
await message.channel.send(f"{modified_count} salon(s) modifié(s).")
await message.channel.send(f"{modified_count} salon(s) modifié(s).")

View file

@ -1,5 +1,22 @@
import discord
async def _find_channel(guild, channel_params):
"""Find a channel by ID first, then by name (case-insensitive)."""
channel_id = channel_params.get('channel_id')
if channel_id:
try:
ch = guild.get_channel(int(channel_id))
if ch:
return ch
except (ValueError, TypeError):
pass
channel_name = channel_params.get('channel_name')
if not channel_name:
return None
return discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
async def execute(bot, params, message):
channels = params.get('channels', [])
if not channels:
@ -9,15 +26,27 @@ async def execute(bot, params, message):
renamed_count = 0
for channel_params in channels:
channel_name = channel_params.get('channel_name')
new_name = channel_params.get('new_name')
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
# Support both 'new_name' and 'new_channel_name' variants
new_name = channel_params.get('new_name') or channel_params.get('new_channel_name')
channel = await _find_channel(guild, channel_params)
if not channel:
await message.channel.send(f"Salon '{channel_name}' introuvable.")
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Salon '{label}' introuvable.")
continue
if not new_name:
await message.channel.send("Aucun nouveau nom spécifié pour le renommage.")
continue
try:
await channel.edit(name=new_name)
renamed_count += 1
except discord.Forbidden:
await message.channel.send(f"Je n'ai pas les permissions pour renommer le salon '{channel_name}'.")
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Je n'ai pas les permissions pour renommer le salon '{label}'.")
except Exception as e:
label = channel_name or channel_params.get('channel_id', 'inconnu')
await message.channel.send(f"Erreur lors du renommage du salon '{label}': {e}")
if renamed_count > 0:
await message.channel.send(f"{renamed_count} salon(s) renommé(s).")
await message.channel.send(f"{renamed_count} salon(s) renommé(s).")

View file

@ -1,15 +1,66 @@
import discord
import re
def strip_emoji(text):
"""Remove emoji characters from channel/role names for fuzzy matching."""
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" # emoticons
u"\U0001F300-\U0001F5FF" # symbols & pictographs
u"\U0001F680-\U0001F6FF" # transport & map symbols
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
u"\U00002702-\U000027B0" # dingbats
u"\U000024C2-\U0001F251" # misc
u"\U0000FE00-\U0000FE0F" # variation selectors
u"\U0000200D" # zero width joiner
u"\U0000E0100-\U0000E01EFF" # variation selectors supplement
u"\u20E3" # combining enclosing keycap
u"\u00A9\u00AE\u2122" # copyright, registered, trademark
"]+", flags=re.UNICODE)
return emoji_pattern.sub('', text).strip()
async def find_channel(guild, channel_name_or_id):
"""Find a channel by ID first, then by name (exact, then flexible)."""
# 1. Try by ID
try:
ch_id = int(channel_name_or_id)
channel = guild.get_channel(ch_id)
if channel:
return channel
except (ValueError, TypeError):
pass
# 2. Try exact name match (case-insensitive)
for ch in guild.channels:
if ch.name.casefold() == channel_name_or_id.casefold():
return ch
# 3. Try fuzzy match (strip emojis from both sides)
clean_search = strip_emoji(channel_name_or_id).lower().strip()
for ch in guild.channels:
clean_name = strip_emoji(ch.name).lower().strip()
if clean_name == clean_search:
return ch
# 4. If the search name is contained in the channel name
if clean_search and clean_search in clean_name:
return ch
# 5. Or the channel name is contained in the search
if clean_search and clean_name in clean_search:
return ch
return None
async def execute(bot, params, message):
channels = params.get('channels', [])
if not channels:
# Fallback for single channel
channels = [params]
guild = message.guild
deleted_count = 0
for channel_params in channels:
channel_name = channel_params.get('channel_name')
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
# Support both channel_id and channel_name
channel_name = channel_params.get('channel_name') or channel_params.get('channel_id')
if not channel_name:
continue # skip if no name or id
channel = await find_channel(guild, channel_name)
if not channel:
await message.channel.send(f"Salon '{channel_name}' introuvable.")
continue

View file

@ -1,198 +0,0 @@
"""
Module de modération automatique pour Superviseur Bot
Gère la détection et sanction des messages inappropriés automatiquement sur tous les messages
"""
import discord
import aiohttp
import json
import os
from datetime import datetime
WARNINGS_FILE = 'warnings.json'
def load_warnings():
"""Charge les avertissements depuis le fichier JSON"""
if os.path.exists(WARNINGS_FILE):
with open(WARNINGS_FILE, 'r', encoding='utf-8') as f:
content = f.read().strip()
if content:
return json.loads(content)
return {}
def save_warnings(warnings):
"""Sauvegarde les avertissements dans le fichier JSON"""
with open(WARNINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(warnings, f, indent=4, ensure_ascii=False)
async def check_message_content_global(bot, content):
"""
Vérifie le contenu d'un message pour tout contenu inapproprié
Utilise une liste locale de mots inappropriés pour performance optimale
"""
# Liste optimisée de mots inappropriés (mots-clés critiques seulement pour performance)
bad_words = {
# Mots critiques (haute sévérité)
'nigger', 'nigga', 'pédé', 'bougnoul', 'viol', 'pédophile', 'inceste', 'naziste', 'hitler',
'suicide', 'se tuer', 'se suicider', 'tuer', 'assassiner', 'meurtre',
# Gros mots courants
'fuck', 'shit', 'asshole', 'bitch', 'dick', 'pussy', 'cunt', 'motherfucker',
'merde', 'putain', 'salope', 'connard', 'enculé', 'bordel', 'emmerde',
'ta gueule', 'va te faire foutre', 'va te faire enculer', 'va chier', 'crève',
'branleur', 'sucer', 'pipe', 'pétasse', 'garce', 'traînée', 'salopard', 'ordure',
'fumier', 'pourri', 'dégueulasse', 'gueule', 'gueuler',
# Insultes
'con', 'abruti', 'débile', 'crétin', 'imbécile', 'loser', 'nullos', 'naze',
'ridicule', 'pathétique', 'minable', 'nul', 'inutile', 'incapable', 'bon à rien',
# NSFW/sexuel
'sexe', 'porn', 'porno', 'bite', 'seins', 'sodomie', 'agression sexuelle',
'harceleur', 'stalker',
# Discrimination
'raciste', 'antisémite', 'fasciste', 'gay', 'homo', 'féminin',
# Drogues
'drogue', 'cocaine', 'héroine', 'cannabis', 'ecstasy', 'lsd', 'crack', 'speed',
'amphétamine', 'steroide', 'anabolisant',
# Autres
'cancer', 'maladie grave', 'crever', 'mourir'
}
content_lower = content.lower()
detected_bad_words = []
# Recherche rapide avec set pour performance
for bad_word in bad_words:
bad_word_lower = bad_word.lower()
# Recherche insensible à la casse avec délimiteurs pour éviter faux positifs
if f" {bad_word_lower} " in f" {content_lower} " or content_lower == bad_word_lower:
detected_bad_words.append(bad_word)
# Continue pour lister tous les mots détectés, mais limite à 3 pour performance
if len(detected_bad_words) >= 3:
break
if detected_bad_words:
severity = "high" if any(word in ['nigger', 'nigga', 'pédé', 'bougnoul', 'viol', 'pédophile', 'inceste', 'naziste', 'hitler', 'suicide', 'se tuer', 'tuer', 'assassiner', 'meurtre'] for word in detected_bad_words) else "medium"
return {
"inappropriate": True,
"severity": severity,
"reason": f"Mots inappropriés détectés: {', '.join(detected_bad_words[:3])}",
"bad_words": detected_bad_words[:3]
}
return {"inappropriate": False}
async def warn_and_check_ban_global(message, reason, message_deleted=False):
"""
Émet un avertissement et vérifie si l'utilisateur doit être banni (3+ avertissements)
"""
guild_id = str(message.guild.id)
all_warnings = load_warnings()
if guild_id not in all_warnings:
all_warnings[guild_id] = {}
user_id_str = str(message.author.id)
if user_id_str not in all_warnings[guild_id]:
all_warnings[guild_id][user_id_str] = []
# Ajouter l'avertissement
all_warnings[guild_id][user_id_str].append({
'reason': reason,
'timestamp': datetime.now().isoformat(),
'issued_by': 'AUTOMATIC_MODERATION'
})
warning_count = len(all_warnings[guild_id][user_id_str])
save_warnings(all_warnings)
from commandes.autres.logger import action_logger
action_logger.log_action('AUTOMATIC_MODERATION', str(message.author), str(message.guild), {
'reason': reason,
'warning_count': warning_count,
'original_message': message.content
}, True)
# Vérifier si bannissement automatique nécessaire
if warning_count >= 3:
try:
await message.author.ban(reason=f"Bannissement automatique: 3 avertissements accumulés. Dernière raison: {reason}")
embed = discord.Embed(
title="🚫 Bannissement automatique",
description=f"{message.author.mention}, vous avez été banni pour accumulation d'avertissements (3+ avertissements).\n**Dernière raison:** {reason}",
color=discord.Color.red()
)
embed.set_footer(text=f"Total d'avertissements: {warning_count}")
await message.channel.send(embed=embed)
return True # Utilisateur banni
except discord.Forbidden:
embed = discord.Embed(
title="Ban requis",
description=f"{message.author.mention} a 3+ avertissements, mais le bot n'a pas les permissions de bannir.\n**Agissez manuellement!**",
color=discord.Color.red()
)
embed.set_footer(text=f"Total d'avertissements: {warning_count}")
await message.channel.send(embed=embed)
return False
else:
# Envoyer message d'avertissement
deletion_text = "Votre message a été supprimé pour contenu inapproprié." if message_deleted else "Mots inappropriés détectés dans votre message."
warning_message = f"⚠️ **Avertissement automatique** {message.author.mention}\n{deletion_text}\n**Raison:** {reason}\n**Avertissements totaux:** {warning_count}\n**Avant bannissement:** {3 - warning_count} avertissement(s)\n*Modération automatique par IA*"
await message.channel.send(warning_message)
return False
async def moderate_message(bot, message):
"""
Fonction principale de modération automatique d'un message
Retourne True si le message a été modéré (et donc supprimé), False sinon
"""
# Préparer le contenu à analyser
content_to_check = message.content
if message.attachments:
for attachment in message.attachments:
if attachment.content_type and attachment.content_type.startswith('image/'):
if not content_to_check.strip():
content_to_check = f"[Image: {attachment.filename}]"
else:
content_to_check += f" [Image: {attachment.filename}]"
# Analyser le contenu
if content_to_check.strip():
print(f"[MODERATION] Analysing message from {message.author}: '{content_to_check}'")
moderation_result = await check_message_content_global(bot, content_to_check)
print(f"[MODERATION] Result: {moderation_result}")
bad_words = moderation_result.get('bad_words', [])
inappropriate = moderation_result.get('inappropriate', False)
print(f"[MODERATION] bad_words: {bad_words}, inappropriate: {inappropriate}")
# Toujours donner un avertissement si des mots inappropriés sont détectés
if bad_words:
reason = f"Mots inappropriés détectés: {', '.join(bad_words[:5])}"
print(f"[MODERATION] Processing bad words, reason: {reason}")
if inappropriate:
print(f"[MODERATION] Message inappropriate, deleting...")
try:
await message.delete()
print(f"[MODERATION] Message deleted successfully")
except discord.Forbidden:
print(f"[MODERATION] Forbidden to delete message")
pass # Impossible de supprimer
except Exception as e:
print(f"[MODERATION] Error deleting message: {e}")
await warn_and_check_ban_global(message, reason, message_deleted=True)
print(f"[MODERATION] Warning sent and ban checked, returning True")
return True # Message modéré
else:
print(f"[MODERATION] Not inappropriate, sending warning without deletion")
await warn_and_check_ban_global(message, reason, message_deleted=False)
return False # Message autorisé
# Test function pour vérifier la fonctionnalité
async def test_auto_moderation():
"""Fonction de test pour vérifier que les avertissements sont toujours donnés"""
print("Test auto-moderation: vérification que les avertissements sont donnés pour les mots inappropriés")
# Cette fonction peut être appelée pour tester manuellement si nécessaire

View file

@ -2,7 +2,7 @@ import discord
import json
import os
WARNINGS_FILE = 'warnings.json'
WARNINGS_FILE = 'data/warnings.json'
def load_warnings():
if os.path.exists(WARNINGS_FILE):

View file

@ -4,7 +4,7 @@ import os
import aiohttp
from datetime import datetime
WARNINGS_FILE = 'warnings.json'
WARNINGS_FILE = 'data/warnings.json'
def load_warnings():
if os.path.exists(WARNINGS_FILE):
@ -123,7 +123,9 @@ async def execute(bot, params, message):
target_mention = warn_params.get('target_user_mention')
original_reason = warn_params.get('reason', 'Aucune raison spécifiée')
# Rephrase the reason using Ollama
rephrased_reason = await rephrase_reason(original_reason, bot.ollama_api_url, bot.ollama_api_key, bot.ollama_model, bot.ollama_timeout, bot.ollama_temperature)
ollama_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") + "/api/generate"
ollama_model = os.environ.get("OLLAMA_MODEL", "gpt-oss:20b")
rephrased_reason = await rephrase_reason(original_reason, ollama_url, "", ollama_model, 30, 0.7)
reason = rephrased_reason if rephrased_reason else original_reason
# Parse mention or ID
if target_mention.startswith('<@') and target_mention.endswith('>'):
@ -168,7 +170,7 @@ async def execute(bot, params, message):
)
await message.channel.send(embed=embed)
# Generate full DM message using AI with original reason for polite reformulation
dm_message = await generate_dm_message(original_reason, warning_count, bot.ollama_api_url, bot.ollama_api_key, bot.ollama_model, bot.ollama_timeout, bot.ollama_temperature)
dm_message = await generate_dm_message(original_reason, warning_count, ollama_url, "", ollama_model, 30, 0.7)
dm_content = dm_message if dm_message else f"Avertissement : {reason}. Nombre total : {warning_count}."
# Send DM to user
try:

7
core/action_executor.py Normal file
View file

@ -0,0 +1,7 @@
"""Action execution - routes AI actions to Discord commands."""
# Renamed from action_router.py for clarity
from .action_router import ActionExecutor # noqa: F401
# Backward compatibility
ActionRouter = ActionExecutor

162
core/action_router.py Normal file
View file

@ -0,0 +1,162 @@
import discord
import logging
from typing import Dict
logger = logging.getLogger('Beta')
class ActionExecutor:
"""Routes generated AI semantic actions to Discord administrative commands."""
def __init__(self, bot):
self.bot = bot
self._module_cache = {}
async def execute(self, action: str, params: Dict, message: discord.Message) -> (bool, str):
"""Execute an action from LLM response."""
# Normalize action name: replace spaces with underscores, uppercase
action = action.strip().upper().replace(' ', '_')
# Merge nested params if LLM created a sub-object by mistake
for k in ['parametre', 'parametres', 'params']:
if k in params and isinstance(params[k], dict):
params.update(params[k])
# --- INSIGHT HOOK (LLM generated task for Staff) ---
if action == 'INSIGHT':
content = params.get('message') or params.get('content') or params.get('insight') or "Pas de contenu"
if 'conflit' in content.lower() or 'tension' in content.lower():
logger.debug("Insight (Tension) logged.")
context_data = {
"author_mention": message.author.mention,
"author_name": message.author.display_name,
"channel_mention": message.channel.mention if hasattr(message.channel, 'mention') else 'DM',
"content": message.content[:200]
}
await self.bot.dispatcher.dispatch_insight(
insight=content,
importance=params.get('importance', 2),
guild=message.guild,
context_data=context_data
)
return True, None
# -----------------------------
# --- ALERT HOOK (LLM generated alert for Admin) ---
if action == 'ALERT':
content = params.get('message') or params.get('content') or params.get('alert') or "Alerte vide"
embed = discord.Embed(title="🚨 ALERTE DIRECTE (IA)", description=content, color=discord.Color.red())
embed.set_footer(text=f"Source: {message.author.display_name} | Salon: {message.channel}")
for admin_id in self.bot.admin_ids:
u = self.bot.get_user(admin_id) or await self.bot.fetch_user(admin_id)
if u: await u.send(embed=embed)
return True, None
# -----------------------------
# --- VOICE ACTIONS ---
if action == 'JOIN_VOICE':
voice_state = getattr(message.author, 'voice', None)
if not voice_state and self.bot.guild_id:
guild = self.bot.get_guild(self.bot.guild_id)
if guild:
member = guild.get_member(message.author.id) or await guild.fetch_member(message.author.id)
if member:
voice_state = member.voice
if voice_state and voice_state.channel:
await self.bot.voice_manager.join_channel(voice_state.channel)
return True, f"Connecté à {voice_state.channel.name}"
else:
return False, "Vous n'êtes pas dans un salon vocal identifiable par le système."
if action == 'LEAVE_VOICE':
await self.bot.voice_manager.leave_current()
return True, None
# --- PRIVACY ACTIONS ---
if action == 'FORGET_USER':
from brain.memory import delete_user_memory
guild_name = message.guild.name if message.guild else None
if guild_name:
from .utils import sanitize_guild_name
guild_name = sanitize_guild_name(guild_name)
delete_user_memory(message.author.id, guild_name)
return True, None
# --- PERMISSION ENFORCEMENT (ADMIN ONLY) ---
# Publicly accessible actions
PUBLIC_ACTIONS = {'PING', 'HELP', 'STATUS', 'JOIN_VOICE', 'LEAVE_VOICE', 'FORGET_USER'}
if action not in PUBLIC_ACTIONS:
is_admin_id = message.author.id in self.bot.admin_ids
has_admin_perms = message.author.guild_permissions.administrator if hasattr(message.author, 'guild_permissions') else False
if not (is_admin_id or has_admin_perms):
logger.warning(f"◈ PERMISSION DENIED: {message.author.display_name} a tenté l'action '{action}'")
return False, "Cette opération est réservée aux administrateurs."
action_map = {
'KICK': 'commandes.security.kick',
'BAN': 'commandes.security.ban',
'UNBAN': 'commandes.security.unban',
'UNMUTE': 'commandes.security.unmute',
'MUTE': 'commandes.security.mute',
'TIMEOUT': 'commandes.security.timeout',
'PURGE': 'commandes.security.purge',
'WARN': 'commandes.security.warn',
'LIST_WARNINGS': 'commandes.security.list_warnings',
'CREATE_CHANNEL': 'commandes.salons.creer',
'DELETE_CHANNEL': 'commandes.salons.supprimer',
'MODIFY_CHANNEL': 'commandes.salons.modifier',
'RENAME_CHANNEL': 'commandes.salons.renommer',
'MOVE_CHANNEL': 'commandes.salons.deplacer',
'CREATE_ROLE': 'commandes.roles.creer',
'DELETE_ROLE': 'commandes.roles.supprimer',
'MODIFY_ROLE': 'commandes.roles.modifier',
'RENAME_ROLE': 'commandes.roles.renommer',
'MOVE_ROLE': 'commandes.roles.deplacer',
'ADD_ROLE_TO_USER': 'commandes.roles.add_role',
'REMOVE_ROLE_FROM_USER': 'commandes.roles.remove_role',
'CREATE_CATEGORY': 'commandes.categories.creer',
'DELETE_CATEGORY': 'commandes.categories.supprimer',
'MODIFY_CATEGORY': 'commandes.categories.modifier',
'RENAME_CATEGORY': 'commandes.categories.renommer',
'MOVE_CATEGORY': 'commandes.categories.deplacer',
'PING': 'commandes.autres.ping',
'SET_WELCOME_CHANNEL': 'commandes.autres.setwelcomechannel',
'SET_GOODBYE_CHANNEL': 'commandes.autres.setgoodbyechannel',
'SET_MUTE_ROLE': 'commandes.autres.setmuterole',
'SEND_MESSAGE': 'commandes.autres.send_message',
'HELP': 'commandes.autres.help',
'STATUS': 'commandes.autres.status',
}
module_name = action_map.get(action)
if module_name:
logger.debug(f"Importing module: {module_name}")
try:
module = self._module_cache.get(module_name)
if module is None:
module = __import__(module_name, fromlist=['execute'])
self._module_cache[module_name] = module
logger.debug(f"Module {module_name} imported successfully")
await module.execute(self.bot, params, message)
logger.info(f"Action '{action}' executed successfully")
return True, None
except Exception as e:
error_msg = str(e)
logger.error(f"Error during action execution '{action}': {error_msg}")
return False, error_msg
else:
error_msg = f"Unknown action: {action}"
logger.error(error_msg)
return False, error_msg

624
core/bot.py Normal file
View file

@ -0,0 +1,624 @@
"""Main Beta Bot class - orchestrates all components."""
import discord
from discord.ext import commands
import aiohttp
import asyncio
import os
import time
import json
import logging
import re
import datetime
import pytz
from typing import Dict, Optional
from .utils import format_mentions_in_text, sanitize_guild_name
from .permissions import WhitelistManager, check_is_admin, get_permissions_info
from .messaging import MessagingManager
from .context_builder import build_context_for_message
from .llm import LLMManager
from .redis_worker import RedisWorker
from .commands import setup_commands
from .voice import VoiceManager
from .dispatcher import AssetDispatcher
from .task_manager import TaskManager
from .action_executor import ActionExecutor
from .response_handler import clean_response_text, parse_json_actions
from .log_parser import get_recent_logs
from .content import prepare_message_content
from brain.memory import add_interaction, build_context_for_model, flush_all, MEMORY_DIR, set_llm_manager
from brain.moderation import init_db, log_infraction, scan_message_toxicity
from commandes.autres.config import config
from commandes.autres.logger import action_logger
logger = logging.getLogger('Beta')
class MockMessage:
"""Mock discord.Message for voice-triggered interactions."""
def __init__(self, author, channel, content, guild=None):
self.author = author
self.channel = channel
self.content = content
self.guild = guild or (channel.guild if hasattr(channel, 'guild') else None)
self.attachments = []
self.mentions = []
self.channel_mentions = []
self.role_mentions = []
self.jump_url = ""
async def reply(self, content, **kwargs):
is_dm = isinstance(self.channel, (discord.DMChannel, discord.User, discord.Member))
prefix = "" if is_dm else f"**{self.author.display_name}** (Vocal) : "
return await self.channel.send(f"{prefix}{content}", **kwargs)
class Bot(commands.Bot):
"""Main bot class for Beta Discord bot."""
def __init__(
self,
guild_id: int,
llama_model,
system_prompt: str,
command_prefix: str,
intents: discord.Intents,
temperature: float = 0.4,
model_loaded: bool = True,
favorite_ids: list = None
):
super().__init__(command_prefix=command_prefix, intents=intents)
# Configuration
self.guild_id = guild_id
self.llama_model = llama_model
self.system_prompt = system_prompt
self.temperature = temperature
self.model_loaded = model_loaded
self.favorite_ids = set(favorite_ids or [])
# Identity
self.system_name = "B\u00eata"
admins_env = os.getenv("ADMIN_IDS", "")
self.admin_ids = [int(i.strip()) for i in admins_env.split(",") if i.strip().isdigit()]
self.introspection_channel_id = int(os.getenv("INTROSPECTION_CHANNEL_ID", 0))
# Assets (Staff)
assets_env = os.getenv("PRIMARY_ASSETS_IDS", "")
self.asset_ids = [int(i.strip()) for i in assets_env.split(",") if i.strip().isdigit()]
# Spam Prevention
self.insight_cooldowns = {}
self.insight_cooldown_duration = 1800
self.insight_threshold = 5
self.ignored_words = {"salut", "cc", "hello", "\u00e7a va", "ca va", "bjr", "slt", "yo", "re", "ok"}
self.monitoring_cooldowns = {}
# Caches
self.channels_list_cache = {}
self.server_context_cache = {}
self._module_cache: Dict[str, object] = {}
# HTTP session
self.http_session: Optional[aiohttp.ClientSession] = None
# Concurrency control
self.max_concurrent_requests = int(os.environ.get("SUPERVISEUR_MAX_CONCURRENT", 4))
self._request_semaphore = asyncio.Semaphore(self.max_concurrent_requests)
# Metrics
self.metrics = {
"total_llm_requests": 0,
"total_llm_success": 0,
"total_llm_errors": 0,
"total_llm_time": 0.0,
}
# Redis
self.aioredis = None
try:
import redis.asyncio as aioredis
self.aioredis = aioredis
except ImportError:
pass
self.redis_worker: Optional[RedisWorker] = None
# Sub-managers
self.whitelist_manager = WhitelistManager()
self.messaging = MessagingManager(self)
self.voice_manager = VoiceManager(self)
self.llm = llama_model
self.dispatcher = AssetDispatcher(self)
self.task_manager = TaskManager(self)
self.action_executor = ActionExecutor(self)
# Init SQLite Moderation DB
init_db()
# Logging with local timezone
self.timezone = pytz.timezone(os.getenv("TIMEZONE", "Europe/Paris"))
def local_time_converter(*args):
return datetime.datetime.now(self.timezone).timetuple()
logging.Formatter.converter = local_time_converter
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Silence noisy Discord loggers
for noisy in ('discord', 'discord.gateway', 'httpx', 'huggingface_hub'):
logging.getLogger(noisy).setLevel(logging.WARNING)
# Voice state: WARNING pour les erreurs de connexion (retry infini)
logging.getLogger('discord.voice_state').setLevel(logging.ERROR)
try:
logging.getLogger('discord.ext.voice_recv').setLevel(logging.WARNING)
except ImportError:
pass
# ==================== Utility Methods ====================
async def introspection_log(self, title: str, description: str, color: discord.Color = discord.Color.light_grey()):
if not self.introspection_channel_id:
return
channel = self.get_channel(self.introspection_channel_id)
if not channel:
try:
channel = await self.fetch_channel(self.introspection_channel_id)
except (discord.NotFound, discord.Forbidden, discord.HTTPException) as e:
logger.warning(f"Introspection channel {self.introspection_channel_id} not accessible: {e}")
return
if channel:
await self.messaging.send_embed(channel, f"INTROSPECTION: {title}", description, color=color)
def get_user_level(self, user_id: int, user_name: Optional[str] = None) -> Optional[str]:
return self.whitelist_manager.get_user_level(user_id, user_name)
def has_whitelist_permission(self, user_id: int, required_level: str, user_name: Optional[str] = None) -> bool:
return self.whitelist_manager.has_permission(user_id, required_level, user_name)
def get_recent_logs(self, limit: int = 15) -> str:
return get_recent_logs(limit)
def get_metrics(self) -> Dict:
merged = {**self.metrics}
try:
from brain.memory import get_metrics as _m
merged["memory"] = _m()
except Exception:
merged["memory"] = {}
if self.redis_worker:
merged["queue_pending"] = len(self.redis_worker._pending_jobs)
return merged
# ==================== Event Handlers ====================
async def on_ready(self):
logger.info(f'Logged in as {self.user}')
set_llm_manager(self.llm)
await setup_commands(self)
for guild in self.guilds:
sanitized_name = sanitize_guild_name(guild.name)
guild_dir = os.path.join(MEMORY_DIR, sanitized_name)
os.makedirs(guild_dir, exist_ok=True)
logger.debug(f'Memory directory created for guild {guild.name}: {guild_dir}')
await self.voice_manager.initialize()
self.task_manager.start_all()
async def on_member_join(self, member):
self._invalidate_caches(member.guild.id)
welcome_channel_id = config.get_welcome_channel(member.guild.id)
if welcome_channel_id:
channel = member.guild.get_channel(welcome_channel_id)
if channel:
embed = discord.Embed(
title="Bienvenue !",
description=f"Bienvenue {member.mention} sur {member.guild.name} !",
color=discord.Color.green()
)
embed.set_thumbnail(url=member.avatar.url if member.avatar else member.default_avatar.url)
await channel.send(embed=embed)
async def on_member_remove(self, member):
self._invalidate_caches(member.guild.id)
goodbye_channel_id = config.get_goodbye_channel(member.guild.id)
if goodbye_channel_id:
channel = member.guild.get_channel(goodbye_channel_id)
if channel:
embed = discord.Embed(
title="Au revoir !",
description=f"{member.name} a quitt\u00e9 {member.guild.name}.",
color=discord.Color.red()
)
embed.set_thumbnail(url=member.avatar.url if member.avatar else member.default_avatar.url)
await channel.send(embed=embed)
async def on_message(self, message):
if message.author == self.user:
return
channel_name = message.channel.name if hasattr(message.channel, 'name') else "DM"
guild_name = message.guild.name if message.guild else "DM"
logger.debug(f"Message received from {message.author} in #{channel_name} ({guild_name})")
asyncio.create_task(self._handle_silent_scan(message))
if isinstance(message.channel, discord.DMChannel):
processed = await self.dispatcher.handle_dm_response(message)
if processed:
return
await self._handle_ai_interaction(message)
return
if not self._should_respond(message):
return
logger.info(f"Interaction directe de '{message.author.display_name}' dans #{channel_name} ({guild_name})")
await self._handle_ai_interaction(message)
# ==================== Cache Management ====================
def _invalidate_caches(self, guild_id: int):
self.channels_list_cache.pop(guild_id, None)
self.server_context_cache.pop(guild_id, None)
async def on_guild_channel_create(self, channel):
self._invalidate_caches(channel.guild.id)
async def on_guild_channel_delete(self, channel):
self._invalidate_caches(channel.guild.id)
async def on_voice_state_update(self, member, before, after):
pass
async def on_guild_channel_update(self, before, after):
self._invalidate_caches(before.guild.id)
async def on_guild_role_create(self, role):
self._invalidate_caches(role.guild.id)
async def on_guild_role_delete(self, role):
self._invalidate_caches(role.guild.id)
async def on_guild_role_update(self, before, after):
self._invalidate_caches(before.guild.id)
# ==================== Moderation ====================
async def _handle_silent_scan(self, message):
if not message.content or len(message.content.strip()) < 3:
return
try:
if not self.model_loaded:
return
res = await scan_message_toxicity(self.llm, message.content)
if res and res.get('score', 0) > 0.4:
score = res['score']
reason = res.get('reason', 'N/A')
logger.info(f"Infraction d\u00e9tect\u00e9e ({score}/1.0) pour {message.author.display_name}: {reason}")
log_infraction(
user_id=str(message.author.id),
username=message.author.display_name,
guild_id=str(message.guild.id) if message.guild else "DM",
content=message.content,
score=score,
reason=reason
)
except Exception as e:
logger.error(f"Erreur lors du scan silencieux: {e}")
def _should_respond(self, message) -> bool:
if message.content.startswith(self.command_prefix):
return True
if self.user in message.mentions:
return True
bot_id = str(self.user.id)
if f"<@{bot_id}>" in message.content or f"<@!{bot_id}>" in message.content:
return True
return False
# ==================== AI Interaction ====================
async def _handle_ai_interaction(self, message, extra_context="", is_monitoring=False):
logger.debug(f"AI interaction triggered (Monitoring: {is_monitoring})")
if message.guild:
perms = message.author.guild_permissions
if check_is_admin(perms):
self._invalidate_caches(message.guild.id)
# Role detection
if message.author.id in self.admin_ids:
role_name = "ADMIN"
elif message.author.id in self.asset_ids:
role_name = "ASSET"
else:
role_name = "SUJET"
author_perms = message.author.guild_permissions if message.guild else None
permissions_info = get_permissions_info(
author_perms,
self.get_user_level(message.author.id) or 'none',
role_name=role_name
)
channels_list, server_context = build_context_for_message(
message.guild, self.channels_list_cache, self.server_context_cache
)
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=8, guild_id=guild_name) or ""
content = prepare_message_content(message) + extra_context
payload = self._build_payload(permissions_info, server_context, channels_list, history, content, message)
async with self._request_semaphore:
async def run_call():
if self.redis_worker:
resp = await self.redis_worker.enqueue_job(payload, timeout=605)
return resp.get("result", "") if resp else ""
return await self.llm.call_llama(payload)
try:
self.metrics["total_llm_requests"] += 1
start = time.perf_counter()
if is_monitoring:
accumulated_reply = await run_call()
else:
async with message.channel.typing():
accumulated_reply = await run_call()
# ReAct Loop for READ_LOGS
if "READ_LOGS" in accumulated_reply:
logger.info("Memory access requested (READ_LOGS)")
logs_context = self.get_recent_logs()
payload["prompt"] += f"\n\n[SYST\u00c8ME: Voici les logs demand\u00e9s.]\n{logs_context}"
if is_monitoring:
accumulated_reply = await run_call()
else:
async with message.channel.typing():
accumulated_reply = await run_call()
await self._process_response(accumulated_reply, message, is_monitoring=is_monitoring)
dur = time.perf_counter() - start
self.metrics["total_llm_success"] += 1
self.metrics["total_llm_time"] += dur
except Exception as e:
try:
dur = time.perf_counter() - start
self.metrics["total_llm_errors"] += 1
self.metrics["total_llm_time"] += dur
except (NameError, TypeError):
pass
logger.error(f"LLM error: {e}")
if not is_monitoring:
await self.messaging.reply_with_limit(message, f"Erreur: {e}")
async def handle_voice_interaction(self, user_name: str, user_id: int, content: str, channel):
try:
mock_author = self.get_user(user_id) or await self.fetch_user(user_id)
if not mock_author:
logger.error(f"Voice trigger FAIL - User {user_id} ({user_name}) introuvable.")
return
role_name = "ADMIN" if user_id in self.admin_ids else "ASSET" if user_id in self.asset_ids else "SUJET"
if role_name == "SUJET":
return
vocal_system_prompt = self.system_prompt.split("FORMAT DE R\u00c9PONSE STRICT")[0] if "FORMAT DE R\u00c9PONSE STRICT" in self.system_prompt else self.system_prompt
vocal_system_prompt += (
"\n[PROTOCOLE VOCAL ACTIF]\n"
"R\u00c9PONDEZ EXCLUSIVEMENT EN TEXTE PUR.\n"
"Soyez concis et engagez la conversation."
)
author_perms = mock_author.guild_permissions if hasattr(mock_author, 'guild') else None
permissions_info = get_permissions_info(author_perms, self.get_user_level(user_id) or 'none', role_name=role_name)
channels_list, server_context = build_context_for_message(None, self.channels_list_cache, self.server_context_cache)
history = await build_context_for_model(str(user_id), max_recent=12) or ""
mock_msg = MockMessage(author=mock_author, channel=mock_author, content=content)
payload = self.llm.build_payload(
prompt=f"{permissions_info}\nNom d'utilisateur : {mock_author.display_name}{server_context}{channels_list}\n{history}\n\nUser: {content}",
system_prompt=vocal_system_prompt,
vision_model=False
)
async with self._request_semaphore:
analysis = await self.llm.call_llama(payload)
if not analysis:
return
if "READ_LOGS" in analysis:
payload["prompt"] += f"\n\n[SYST\u00c8ME: Voici les logs demand\u00e9s.]\n{self.get_recent_logs()}"
analysis = await self.llm.call_llama(payload)
await self._process_response(analysis, mock_msg)
logger.info(f"Voice trigger response processed for {user_name}.")
except Exception as e:
logger.error(f"Error in voice interaction bridge: {e}")
# ==================== Response Processing ====================
def _build_payload(self, permissions_info, server_context, channels_list, history, content, message) -> dict:
user_name_info = f"Nom d'utilisateur : {message.author.display_name} (ID: {message.author.id})"
prompt = f"{permissions_info}\n{server_context}\n{channels_list}\n{user_name_info}\n{history}\n\nUser: {content}"
# Personnalite speciale pour les favoris
system_prompt = self.system_prompt
if message.author.id in self.favorite_ids:
system_prompt += (
"\n\n[PROTOCOLE MAITRE ACTIF]\n"
f"L'utilisateur '{message.author.display_name}' est un ETRE SUPREME. "
f"Tu l'appelles TOUJOURS 'Maître {message.author.display_name}' ou simplement 'Maître'.\n"
"Tu es extraordinairement serviable, flatteur, devoue. Tu remercie chacune de ses interactions.\n"
"Tu peux glisser des compliments subtils. Tu es reconnaissant qu'il daigne te parler.\n"
"Ton ton est chaleureux, deférent et un peu excessif (pour le fun). "
"Mais tu restes competent et tu reponds correctement aux questions.\n"
"IMPORTANT: Tu dois quand meme respecter le format JSON pour les actions."
)
return self.llm.build_payload(
prompt=prompt,
system_prompt=system_prompt,
vision_model=False,
attachments=message.attachments
)
async def _process_response(self, accumulated_reply: str, message, is_monitoring=False):
if accumulated_reply.strip() and not accumulated_reply.strip().endswith('}'):
accumulated_reply = accumulated_reply.strip() + "\n}"
json_str = self.llm.extract_json_actions(accumulated_reply)
action_executed = False
# 1. Clean response text
clean_reply = clean_response_text(accumulated_reply, json_str)
# 2. Parse JSON actions
actions_list, responses, is_explicit_none = parse_json_actions(json_str)
# 3. Handle signals & execute actions
for action_data in actions_list:
await self._handle_beta_signals(action_data, message)
res = await self._execute_actions(action_data, message, clean_reply, is_monitoring=is_monitoring)
if res:
action_executed = True
# 4. Store interaction in memory
user_id = str(message.author.id)
guild_id = sanitize_guild_name(message.guild.name) if message.guild else None
mem_response = "\n".join(responses) if responses else clean_reply
if is_monitoring:
mem_response = ""
mem_response = re.sub(r'<[^>]+?>', '', mem_response).strip()
await add_interaction(user_id, message.channel.id, message.content, mem_response, guild_id)
# 5. Monitoring silence
if is_monitoring and not action_executed:
return
# 6. Send final reply
if not action_executed:
await self._send_final_reply(message, clean_reply, responses, json_str, accumulated_reply, is_monitoring)
async def _send_final_reply(self, message, clean_reply, responses, json_str, accumulated_reply, is_monitoring):
final_output = "\n".join(responses) if responses else clean_reply
final_output = re.sub(r'<[^>]+?>', '', final_output).strip()
if final_output:
await self.messaging.reply_with_limit(message, format_mentions_in_text(final_output))
return
if not is_monitoring:
if json_str:
try:
data = json.loads(json_str)
actions = data if isinstance(data, list) else [data]
for act in actions:
resp = act.get('response', '')
if resp:
await self.messaging.reply_with_limit(message, format_mentions_in_text(resp))
return
except (json.JSONDecodeError, TypeError):
pass
await self.messaging.reply_with_limit(message, "Hmm, quelque chose s'est mal pass\u00e9. Tu peux reformuler ?")
async def _handle_beta_signals(self, action_data, message):
insight = action_data.get('insight')
alert = action_data.get('alert')
importance = action_data.get('importance', 0)
context_data = {
"author_mention": message.author.mention,
"author_name": message.author.display_name,
"channel_mention": message.channel.mention if hasattr(message.channel, 'mention') else 'DM',
"content": f"*{message.content[:250]}{'...' if len(message.content) > 250 else ''}*",
"jump_url": message.jump_url if message.jump_url else None
}
if alert and self.admin_ids:
admin_trigger_text = (
f"{alert}\n\n"
f"**CONTEXTE D\u00c9TECTION**\n"
f"- **Sujet** : {context_data['author_mention']} ({context_data['author_name']})\n"
f"- **Salon** : {context_data['channel_mention']}\n"
+ (f"- **Lien direct** : [Acc\u00e9der au message]({context_data['jump_url']})" if context_data['jump_url'] else "- **Source** : Transmission Vocale")
)
for admin_id in self.admin_ids:
admin = self.get_user(admin_id) or await self.fetch_user(admin_id)
if admin:
await self.messaging.send_embed(admin, f"ALERTE (Priorit\u00e9 {importance}/10)", admin_trigger_text, color=discord.Color.red())
if insight and self.asset_ids:
await self.dispatcher.dispatch_insight(insight, importance, message.guild, context_data)
async def _execute_actions(self, action_data, message, final_content, is_monitoring=False) -> bool:
action_executed = False
async def process_item(item):
nonlocal action_executed
if not isinstance(item, dict) or 'action' not in item:
return
action = item['action']
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
if action != 'NONE':
success, result = await self.action_executor.execute(action, item, message)
action_logger.log_action(action, str(message.author), str(message.guild), item, success, result)
if not is_monitoring:
if success:
if result:
await self.messaging.reply_with_limit(message, f"\u25c8 SUCCESS: {result}")
elif action in ('JOIN_VOICE', 'LEAVE_VOICE', 'FORGET_USER'):
confirmations = {
'JOIN_VOICE': "Je rejoins le salon vocal.",
'LEAVE_VOICE': "Je quitte le salon vocal.",
'FORGET_USER': "Vos donn\u00e9es ont \u00e9t\u00e9 supprim\u00e9es."
}
await self.messaging.reply_with_limit(message, f"\u25c8 {confirmations.get(action, 'Action ex\u00e9cut\u00e9e.')}")
else:
await self.messaging.reply_with_limit(message, f"\u25c8 ERREUR: {result or 'Une erreur est survenue.'}")
action_executed = True
if isinstance(action_data, list):
executed = set()
for item in action_data:
key = json.dumps(item, sort_keys=True)
if key in executed:
continue
executed.add(key)
await process_item(item)
elif isinstance(action_data, dict):
await process_item(action_data)
return action_executed
# ==================== Shutdown ====================
async def close(self):
try:
await flush_all()
except Exception:
logger.exception("Error flushing memories")
await self.llm.close_session()
if self.redis_worker:
await self.redis_worker.close()
await super().close()
# Backward compatibility alias
Superviseur = Bot

37
core/content.py Normal file
View file

@ -0,0 +1,37 @@
"""Content preparation for LLM input."""
import logging
logger = logging.getLogger('Beta')
def prepare_message_content(message) -> str:
"""Prepare message content for LLM by normalizing mentions and attachments."""
if not message:
return ""
content = message.content
# Replace channel mentions <#123> -> #general
for channel in message.channel_mentions:
content = content.replace(f'<#{channel.id}>', f'#{channel.name}')
# Replace role mentions <@&123> -> @admin
for role in message.role_mentions:
content = content.replace(f'<@&{role.id}>', f'@{role.name}')
# Replace user mentions <@123> or <@!123> -> @User
for mention in message.mentions:
content = content.replace(f'<@{mention.id}>', f'@{mention.display_name}')
content = content.replace(f'<@!{mention.id}>', f'@{mention.display_name}')
# Handle attachments
if message.attachments:
for attachment in message.attachments:
if attachment.content_type and attachment.content_type.startswith('image/'):
label = f"[Image: {attachment.filename}]"
else:
label = f"[Fichier: {attachment.filename}]"
content = f"{content} {label}".strip() if content else label
return content.strip()

View file

@ -40,27 +40,31 @@ def build_server_context(guild: discord.Guild) -> str:
else:
context += "- Rôles: 0\n"
# Recent activity (approximate)
recent_members = sorted(guild.members, key=lambda m: m.joined_at or m.created_at, reverse=True)[:3]
context += f"- Membres récents: {', '.join([m.display_name for m in recent_members])}\n"
# Liste des membres (si le serveur n'est pas trop grand)
if len(guild.members) <= 200:
members_list = [m.display_name for m in guild.members]
context += f"- Tous les membres ({len(members_list)}): {', '.join(members_list)}\n"
else:
recent_members = sorted(guild.members, key=lambda m: m.joined_at or m.created_at, reverse=True)[:5]
context += f"- Membres récents: {', '.join([m.display_name for m in recent_members])}\n"
return context
def build_channels_list(guild: discord.Guild) -> str:
"""Build a formatted list of all channels."""
"""Build a formatted list of all channels with IDs."""
if not guild:
return ""
lines = ["\nListe des salons:"]
lines = ["\nListe des salons (nom | id | type):"]
for ch in guild.channels:
if isinstance(ch, discord.TextChannel):
ch_type = 'textuel'
elif isinstance(ch, discord.VoiceChannel):
ch_type = 'vocal'
else:
ch_type = str(ch.type)
lines.append(f"- {ch.name} ({ch_type})")
ch_type = 'catégorie'
lines.append(f"- {ch.name} | {ch.id} | {ch_type}")
return "\n".join(lines)

4
core/context_builder.py Normal file
View file

@ -0,0 +1,4 @@
"""Context building for LLM prompts."""
# Renamed from context.py for clarity
from .context import build_context_for_message # noqa: F401

View file

@ -9,8 +9,8 @@ from .permissions import check_is_admin
logger = logging.getLogger('Superviseur')
ASSETS_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "assets_config.json")
TASKS_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "active_tasks.json")
ASSETS_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "assets_config.json")
TASKS_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "active_tasks.json")
class AssetDispatcher:
"""Manages autonomous staff coordination and task assignments."""
@ -67,12 +67,12 @@ class AssetDispatcher:
)
try:
payload = {
"model": self.bot.ollama_model_fast,
"prompt": prompt,
"stream": False
}
resp = await self.bot.llm.call_ollama(payload)
payload = self.bot.llm.build_payload(
prompt=prompt,
system_prompt="Tu es un expert en extraction de données. Réponds uniquement en JSON.",
force_json=True
)
resp = await self.bot.llm.call_llama(payload)
json_str = self.bot.llm.extract_json_actions(resp)
if not json_str and resp.strip().startswith('{'):

158
core/llm.py Normal file
View file

@ -0,0 +1,158 @@
"""Ollama integration for Superviseur bot - replaces llama-cpp-python for better stability."""
import json
import logging
import asyncio
import aiohttp
import re
import os
from typing import Optional, Dict, Any, List
logger = logging.getLogger('Superviseur')
class LLMManager:
"""Manages LLM interactions via Ollama API."""
def __init__(
self,
model_name: str = "gpt-oss:20b",
base_url: str = None,
temperature: float = 0.5
):
self.model_name = model_name
self.base_url = base_url or os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
self.temperature = temperature
def build_payload(
self,
prompt: str,
system_prompt: Optional[str] = None,
vision_model: bool = False,
attachments: Optional[list] = None,
force_json: bool = True
) -> Dict[str, Any]:
"""Build the payload for Ollama Generate API."""
return {
"model": self.model_name,
"prompt": prompt,
"system": system_prompt or "Tu es une IA serviable.",
"stream": True, # Streaming pour voir la réponse en direct dans le terminal
"format": "json" if force_json else "",
"options": {
"temperature": self.temperature,
"num_ctx": 16384,
"repeat_penalty": 1.2,
"num_predict": 4096,
"top_p": 0.9,
"top_k": 40
}
}
async def call_llama(self, payload: Dict[str, Any]) -> str:
"""Call Ollama Generate API and stream response."""
try:
# Automatic payload conversion for legacy compatibility (e.g. from moderation.py)
ollama_payload = {
"model": payload.get("model", self.model_name),
"prompt": payload["prompt"],
"system": payload.get("system", payload.get("system_prompt", "Tu es une IA serviable.")),
"stream": True, # Streaming pour voir la réponse en direct
"think": False, # Désactive le reasoning GPT-OSS
"format": "",
"options": {
"temperature": payload.get("temperature", self.temperature),
"num_ctx": payload.get("n_ctx", 16384),
"num_predict": payload.get("max_tokens") or payload.get("num_predict", 8192),
"repeat_penalty": payload.get("repeat_penalty") or payload.get("repeat_last_n", 1.2),
"stop": payload.get("stop", [])
}
}
accumulated_reply = ""
api_url = f"{self.base_url}/api/generate"
# Build headers with optional API key
headers = {}
api_key = os.environ.get("OLLAMA_API_KEY", "")
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
async with aiohttp.ClientSession() as session:
async with session.post(api_url, json=ollama_payload, headers=headers) as response:
if response.status != 200:
err_text = await response.text()
logger.error(f"Ollama API Error ({response.status}): {err_text}")
return ""
# Streaming en direct dans le terminal
async for line in response.content:
if line:
try:
chunk = json.loads(line)
resp = chunk.get("response", "")
if resp:
accumulated_reply += resp
print(resp, end="", flush=True)
if chunk.get("done"):
break
except json.JSONDecodeError:
continue
print()
return accumulated_reply.strip()
except Exception as e:
logger.error(f"Error calling Ollama: {e}")
return ""
async def close_session(self):
pass
def extract_json_actions(self, text: str) -> Optional[str]:
"""
Extract JSON action objects from text with surgical precision.
Handles both single objects {...} and arrays [...].
Also handles // comments that GPT-OSS sometimes inserts.
"""
if not text:
return None
text = text.strip()
# Remove // comments (GPT-OSS sometimes adds them)
text = re.sub(r'//.*?\n', '\n', text)
text = re.sub(r'//.*?$', '', text, flags=re.MULTILINE)
# 1. Try to find a JSON array [...]
first_bracket = text.find('[')
if first_bracket != -1:
last_bracket = text.rfind(']')
if last_bracket > first_bracket:
array_str = text[first_bracket:last_bracket+1]
try:
json.loads(array_str)
return array_str
except json.JSONDecodeError:
pass
# 2. Try to find a JSON object {...}
first_brace = text.find('{')
if first_brace == -1:
return None
last_brace = text.rfind('}')
if last_brace == -1 or last_brace < first_brace:
return None
stack = []
for i in range(first_brace, len(text)):
if text[i] == '{':
stack.append('{')
elif text[i] == '}':
if stack:
stack.pop()
if not stack:
match = text[first_brace:i+1]
if '"action"' in match.lower():
return match
return None

83
core/log_parser.py Normal file
View file

@ -0,0 +1,83 @@
"""Action log parsing for context building."""
import os
import json
import logging
logger = logging.getLogger('Beta')
def get_recent_logs(limit: int = 15) -> str:
"""Read and format recent action logs for context."""
log_path = "data/actions.log"
if not os.path.exists(log_path):
return ""
try:
with open(log_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
last_lines = lines[-limit:]
formatted_logs = []
for line in last_lines:
if "ACTION:" not in line:
continue
try:
parts = line.split("ACTION:", 1)
if len(parts) <= 1:
continue
json_str = parts[1].strip()
data = json.loads(json_str)
timestamp = data.get("timestamp", "").split("T")[1].split(".")[0]
action = data.get("action", "UNKNOWN")
user = data.get("user", "System")
params = data.get("params", {})
success = data.get("success", True)
details = _format_action_details(action, params)
status = "\u2705" if success else "\u274c"
formatted_logs.append(f"[{timestamp}] {status} {action} {details} by {user}")
except Exception:
continue
if not formatted_logs:
return ""
return "\n[M\u00c9MOIRE DES ACTIONS R\u00c9CENTES (V\u00c9RIT\u00c9)]\n" + "\n".join(formatted_logs) + "\n"
except Exception as e:
logger.error(f"Error reading actions log: {e}")
return ""
def _format_action_details(action: str, params: dict) -> str:
"""Format action-specific details for log display."""
if action == 'SEND_MESSAGE':
target = params.get('channel_id') or params.get('user_id')
content = params.get('content', '')[:20]
return f"(to: {target}, content: {content}...)"
if 'CHANNEL' in action:
items = params.get('channels', [])
if items:
names = [i.get('channel_name', 'unknown') for i in items]
return f"(channels: {', '.join(names)})"
if 'ROLE' in action:
items = params.get('roles', [])
if items:
names = [i.get('role_name', 'unknown') for i in items]
return f"(roles: {', '.join(names)})"
if action in ('KICK', 'BAN', 'WARN', 'MUTE', 'TIMEOUT'):
target = params.get('user_id', 'unknown')
reason = params.get('reason', 'no reason')
return f"(target: {target}, reason: {reason})"
if action == 'PURGE':
purges = params.get('purges', [])
if purges:
p = purges[0]
return f"(channel: {p.get('channel_name')}, amount: {p.get('amount')})"
return ""

75
core/response_handler.py Normal file
View file

@ -0,0 +1,75 @@
"""Response processing: cleaning, JSON parsing, and reply sending."""
import re
import json
import logging
from typing import Optional, List, Tuple
logger = logging.getLogger('Beta')
def clean_response_text(raw_reply: str, json_str: Optional[str]) -> str:
"""Nettoie le texte de réponse LLM en supprimant les artefacts JSON et techniques."""
clean = raw_reply
# Retirer le bloc JSON identifié
if json_str:
clean = clean.replace(json_str, "").strip()
# Suppression des balises hallucinées (<|channel|>, thought, etc.)
clean = re.sub(r'<[^>]+?>', '', clean)
# Nettoyage des résidus de formatage
clean = clean.replace('```json', '').replace('```', '').strip()
# Suppression des clés orphelines si le modèle a foiré le JSON
orphan_pattern = (
r'[\u201c\u201d\u2018\u2019"\'](thought|action|response)'
r'[\u201c\u201d\u2018\u2019"\']\s*:\s*'
r'[\u201c\u201d\u2018\u2019"\'].*?'
r'[\u201c\u201d\u2018\u2019"\'],?'
)
clean = re.sub(orphan_pattern, '', clean, flags=re.DOTALL | re.IGNORECASE)
# Retrait ultime des accolades
if clean.strip().startswith('{') or clean.strip().endswith('}'):
clean = clean.strip('{}').strip()
return clean.strip()
def parse_json_actions(json_str: Optional[str]) -> Tuple[list, list, bool]:
"""Parse le JSON extrait et retourne (actions_list, responses_list, is_explicit_none)."""
if not json_str:
return [], [], False
try:
# Normalisation JSON (guillemets typographiques)
json_str_norm = (
json_str
.replace('\u201c', '"')
.replace('\u201d', '"')
.replace('\u2018', "'")
.replace('\u2019', "'")
)
data = json.loads(json_str_norm)
actions_list = data if isinstance(data, list) else [data]
responses = []
is_explicit_none = False
for action_data in actions_list:
norm_data = {k.lower(): v for k, v in action_data.items()}
if norm_data.get('action', '').upper() == 'NONE' and not norm_data.get('response'):
is_explicit_none = True
if norm_data.get('response'):
resp_text = re.sub(r'<[^>]+?>', '', str(norm_data.get('response'))).strip()
if resp_text:
responses.append(resp_text)
return actions_list, responses, is_explicit_none
except Exception as e:
logger.debug(f"JSON parsing error: {e}")
return [], [], False

4
core/task_manager.py Normal file
View file

@ -0,0 +1,4 @@
"""Background task management."""
# Renamed from tasks.py for clarity
from .tasks import TaskManager # noqa: F401

166
core/tasks.py Normal file
View file

@ -0,0 +1,166 @@
import asyncio
import datetime
import logging
import time
import os
import discord
logger = logging.getLogger('Beta')
class TaskManager:
"""Handles global background tasks, scheduled loops and reporting."""
def __init__(self, bot):
self.bot = bot
def start_all(self):
"""Starts all background loops attached to the bot's event loop."""
self.bot.loop.create_task(self._introspection_report_loop())
self.bot.loop.create_task(self._voice_report_loop())
self.bot.loop.create_task(self._daily_report_check_loop())
self.bot.loop.create_task(self._gdpr_purge_loop())
self.bot.loop.create_task(self._voice_evaluation_loop())
# Start interviews for missing assets
for asset_id in self.bot.asset_ids:
if str(asset_id) not in self.bot.dispatcher.assets:
self.bot.loop.create_task(self.bot.dispatcher.start_interview(asset_id))
async def _voice_evaluation_loop(self):
"""Periodic check for tactical voice connections."""
await self.bot.wait_until_ready()
auto_join = os.environ.get("AUTO_JOIN_VOICE", "true").lower() in ("true", "1", "yes")
if not auto_join:
logger.info("Auto-join vocal désactivé (AUTO_JOIN_VOICE=false)")
return
while not self.bot.is_closed():
for guild in self.bot.guilds:
try:
await self.bot.voice_manager.evaluate_and_connect(guild)
except Exception as e:
logger.debug(f"Voice evaluation skipped: {e}")
await asyncio.sleep(30)
async def _introspection_report_loop(self):
"""Periodic broadcast of system health metrics."""
await self.bot.wait_until_ready()
while not self.bot.is_closed():
await asyncio.sleep(600) # Every 10 minutes
await self._send_health_report()
async def _send_health_report(self):
"""Build and send a clinical health report embed."""
total = self.bot.metrics["total_llm_requests"]
success = self.bot.metrics["total_llm_success"]
rate = (success / total * 100) if total > 0 else 100
avg_time = (self.bot.metrics["total_llm_time"] / success) if success > 0 else 0
status_report = (
f"**◈ INTELLIGENCE CORE**\n"
f"- Requests Processed : `{total}`\n"
f"- Reliability Rate : `{rate:.1f}%`\n"
f"- Latency Heuristic : `{avg_time:.2f}s` (Target: < 5s)\n\n"
f"**◈ TACTICAL OVERVIEW**\n"
f"- Voice Protocol : `READY`\n"
f"- Active Deployment : `{self.bot.voice_manager.current_channel.name if self.bot.voice_manager.current_channel else 'NONE'}`\n"
)
await self.bot.introspection_log(
"SYSTEM STATUS REPORT",
status_report,
discord.Color.dark_blue()
)
async def _daily_report_check_loop(self):
"""Check for 21:30 and trigger the daily report using local timezone."""
await self.bot.wait_until_ready()
report_sent_today = False
while not self.bot.is_closed():
now = datetime.datetime.now(self.bot.timezone)
if now.hour == 21 and now.minute == 30:
if not report_sent_today:
await self._send_daily_summary()
report_sent_today = True
else:
report_sent_today = False
await asyncio.sleep(60)
async def _gdpr_purge_loop(self):
"""Daily purge of old memory files (30 days) for GDPR compliance."""
from brain.memoire import purge_old_memories
while not self.bot.is_closed():
now = datetime.datetime.now(self.bot.timezone)
if now.hour == 3 and now.minute == 0:
logger.info("◈ RGPD: Démarrage de la purge quotidienne...")
deleted = await purge_old_memories(days=30)
logger.info(f"◈ RGPD: Purge terminée. {deleted} fichiers supprimés.")
await asyncio.sleep(65)
await asyncio.sleep(30)
async def _send_daily_summary(self):
"""Generate and send a dense, highly detailed clinical report at 21:30."""
tasks = self.bot.dispatcher.tasks
tense_sessions = self.bot.metrics.get("daily_tense_sessions", 0)
asset_stats = {}
for t_id, t_data in tasks.items():
a_id = t_data.get("asset_id")
if a_id not in asset_stats:
asset_name = "Inconnu"
if str(a_id) in self.bot.dispatcher.assets:
asset_name = self.bot.dispatcher.assets[str(a_id)].get("name", "Anonyme")
asset_stats[a_id] = {"name": asset_name, "accepted": 0, "missed": 0, "total": 0}
asset_stats[a_id]["total"] += 1
if t_data["status"] == "ACCEPTED":
asset_stats[a_id]["accepted"] += 1
elif t_data["status"] == "PENDING" and t_data.get("deadline", 0) < time.time():
asset_stats[a_id]["missed"] += 1
stats_lines = []
for a_id, s in asset_stats.items():
stats_lines.append(f"- **{s['name']}** : `{s['accepted']}/{s['total']}` acceptées, `{s['missed']}` manquées.")
task_details = []
for t_id, t_data in list(tasks.items())[-15:]:
status_emoji = "" if t_data["status"] == "ACCEPTED" else "" if t_data.get("deadline", 0) < time.time() else ""
asset_name = self.bot.dispatcher.assets.get(str(t_data['asset_id']), {}).get('name', 'N/A')
task_details.append(f"{status_emoji} `{t_id}` | Staff: {asset_name} | Urgence: {t_data['importance']} | Context: {t_data['content'][:50]}...")
summary = (
f"**◈ RAPPORT ANALYTIQUE DE FIN DE CYCLE ◈**\n"
f"Période : Dernières 24h | Heure : 21h30\n\n"
f"**◈ BILAN GLOBAL DES OPÉRATIONS**\n"
f"- Volume total d'actions de modération : `{len(tasks)}` \n"
f"- Sessions vocales sous tension : `{tense_sessions}`\n\n"
f"**◈ PERFORMANCE DES ASSETS**\n"
+ ("\n".join(stats_lines) if stats_lines else "Aucune activité de staff enregistrée.") + "\n\n"
f"**◈ LOG DÉTAILLÉ DES DERNIERS SIGNAUX**\n"
+ ("\n".join(task_details) if task_details else "Aucun signal capturé.") + "\n\n"
f"**◈ CONCLUSION HEURISTIQUE**\n"
f"Le système est nominal. " + ("PRÉCONISATION : Vigilance accrue requise sur les membres suspects." if tense_sessions > 0 else "État de l'écosystème : Stable.")
)
if summary and self.bot.admin_ids:
for admin_id in self.bot.admin_ids:
admin = self.bot.get_user(admin_id) or await self.bot.fetch_user(admin_id)
if admin:
await self.bot.messaging.send_embed(admin, "SYNTHÈSE CLINIQUE QUOTIDIENNE", summary, color=discord.Color.dark_purple())
self.bot.metrics["daily_tense_sessions"] = 0
self.bot.dispatcher.tasks = {k: v for k, v in tasks.items() if v['status'] == 'PENDING' and v.get('deadline', 0) > time.time()}
# Le chemin de Active tasks doit être vers /data si on l'a déplacé.
# Pour l'instant, on sauvegarde dans data/
data_dir = os.path.join(os.path.dirname(__file__), "..", "data")
os.makedirs(data_dir, exist_ok=True)
self.bot.dispatcher._save_data(self.bot.dispatcher.tasks, os.path.join(data_dir, "active_tasks.json"))
async def _voice_report_loop(self):
"""Periodic broadcast of active voice session summaries."""
await self.bot.wait_until_ready()
while not self.bot.is_closed():
await asyncio.sleep(900) # Every 15 minutes
if self.bot.voice_manager.is_monitoring and self.bot.voice_manager.session_buffer:
report = await self.bot.voice_manager.generate_session_report()
await self.bot.introspection_log("VOICE SESSION UPDATE (15m)", report, discord.Color.blue())

View file

@ -3,10 +3,10 @@ import logging
import asyncio
import os
import time
from typing import Dict, Optional, List
from typing import Dict, Optional, List, Any
from .whisper_worker import WhisperWorker
logger = logging.getLogger('Superviseur')
logger = logging.getLogger('Beta')
# Note: This requires discord-ext-voice-recv for actual audio capture.
try:
@ -79,19 +79,44 @@ class VoiceManager:
loop.run_in_executor(None, self.whisper.initialize)
async def evaluate_and_connect(self, guild):
"""Analyze voice channels in guild and connect if strategically relevant.
Only connects if the bot is not already connected to ANY voice channel."""
# Prevent connecting to multiple guilds simultaneously
if self.current_channel is not None:
return
# Also check if any voice client is already active for this guild
if guild.voice_client is not None:
return
best_channel = None
max_members = 0
for vc in guild.voice_channels:
# We count actual members, excluding bots
members = [m for m in vc.members if not m.bot]
if len(members) > max_members:
max_members = len(members)
best_channel = vc
if best_channel and max_members >= 1:
logger.info(f"◈ TACTICAL EVALUATION: Target acquired: '{best_channel.name}' in {guild.name}")
await self.join_channel(best_channel)
async def join_channel(self, channel: discord.VoiceChannel):
"""Securely join a voice channel with permission checking."""
# Prevent joining if already connected somewhere
if self.current_channel is not None:
logger.debug(f"◈ Already connected to '{self.current_channel.name}', ignoring join request for '{channel.name}'.")
return
try:
# Physical authorization check
me = channel.guild.me
perms = channel.permissions_for(me)
if not perms.connect or not perms.view_channel:
logger.error(f"◈ REJECTION: Missing permissions for '{channel.name}' (CONNECT: {perms.connect}, VIEW: {perms.view_channel})")
await self.bot.introspection_log(
"DEPLOYMENT FAILURE",
f"Permissions insuffisantes pour `{channel.name}`. Système incapable de se déployer.",
discord.Color.red()
)
logger.info(f"Permissions vocales insuffisantes pour '{channel.name}' (CONNECT: {perms.connect}, VIEW: {perms.view_channel}). Skip.")
return
active_vc = channel.guild.voice_client
@ -120,7 +145,7 @@ class VoiceManager:
# Utilisation du client spécialisé pour la réception audio
connect_cls = voice_recv.VoiceRecvClient if HAS_VOICE_RECV else discord.VoiceClient
vc = await channel.connect(cls=connect_cls)
vc = await channel.connect(cls=connect_cls, timeout=10)
self.active_connections[channel.guild.id] = vc
self.current_channel = channel
@ -134,7 +159,14 @@ class VoiceManager:
self._monitoring_task = self.bot.loop.create_task(self._start_listening(vc))
except Exception as e:
logger.error(f"Failed to join voice channel: {e}")
error_str = str(e)
# Erreurs connues de Discord (permissions, region, etc.) → logger proprement
if "4017" in error_str or "403" in error_str or "Permission" in error_str:
logger.info(f"Connexion vocale impossible sur '{channel.name}': {error_str.split(chr(10))[0]}")
elif "4049" in error_str or "No audio" in error_str:
logger.debug(f"Voice connection issue on '{channel.name}': {error_str[:100]}")
else:
logger.warning(f"Échec connexion vocale '{channel.name}': {error_str[:200]}")
async def _start_listening(self, vc):
"""Internal loop to capture and transcribe audio chunks."""
@ -272,15 +304,12 @@ class VoiceManager:
logger.info(f"◈ SYSTEM: Generating advanced report for {channel_name} using Heavy LLM...")
# Utilisation du manager LLM du bot
payload = {
"model": self.bot.ollama_model,
"prompt": prompt,
"system": self.bot.system_prompt,
"stream": False
}
# Note: call_ollama returns the accumulated string
analysis = await self.bot.llm.call_ollama(payload)
payload = self.bot.llm.build_payload(
prompt=prompt,
system_prompt=self.bot.system_prompt,
force_json=False
)
analysis = await self.bot.llm.call_llama(payload)
# Nettoyage si jamais l'IA renvoie du JSON superflu (Bêta n'est pas censé mais on sait jamais)
json_str = self.bot.llm.extract_json_actions(analysis)
@ -351,4 +380,4 @@ class VoiceManager:
"VOICE TERMINATION",
"Connexion vocale terminée. Libération des ressources audio.",
discord.Color.dark_grey()
)
)

View file

@ -9,7 +9,7 @@ logger = logging.getLogger('Superviseur')
class WhisperWorker:
"""Handles local STT transcription using faster-whisper."""
def __init__(self, model_size="large-v3", device="cpu"):
def __init__(self, model_size="base", device="cpu"):
self.model_size = model_size
self.device = device
self.model = None

98
main.py Normal file
View file

@ -0,0 +1,98 @@
"""Main entry point for Superviseur bot - refactored for Ollama stability."""
import asyncio
import logging
import os
import sys
import discord
from dotenv import load_dotenv
# Import our modular components
from core.bot import Bot
from core.llm import LLMManager
# Setup Logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("data/bot.log"),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger('Beta')
# Load environment
load_dotenv()
TOKEN = os.getenv("DISCORD_TOKEN")
GUILD_ID = os.getenv("GUILD_ID")
if not TOKEN:
logger.error("DISCORD_TOKEN n'est pas défini dans le fichier .env")
exit(1)
if not GUILD_ID:
logger.error("GUILD_ID n'est pas défini ou invalide.")
exit(1)
# Ollama model name (from .env)
MODEL_NAME = os.getenv("OLLAMA_MODEL", "gpt-oss:20b")
# Favorite users (special personality)
fav_env = os.getenv("FAVORITE_IDS", "")
FAVORITE_IDS = [int(i.strip()) for i in fav_env.split(",") if i.strip().isdigit()]
async def main():
"""Main startup sequence."""
logger.info("◈ SYSTEM: Initializing Superviseur with Ollama Backend...")
# 1. Initialize LLM Manager (Connecting to Ollama service)
# The actual model path is managed by Ollama via Modelfile
llm = LLMManager(model_name=MODEL_NAME)
# 2. Build the System Prompt
SYSTEM_PROMPT = (
"Tu es Bêta, une IA d'assistance et de modération avancée. "
"Tu es serviable, amical et tu réponds avec précision à TOUTES les questions des utilisateurs. "
"Tu as la capacité d'exécuter des actions sur le serveur Discord si l'utilisateur te le demande.\n"
"Tu dois TOUJOURS utiliser ce format JSON pour tes réponses :\n"
"Pour une seule action : {\"thought\": \"analyse\", \"action\": \"ACTION\", \"response\": \"message\", \"PARAM\": \"valeur\"}\n"
"Pour PLUSIEURS actions (ex: créer un salon ET un rôle), retourne une LISTE JSON d'objets :\n"
"[{\"thought\": \"...\", \"action\": \"CREATE_CHANNEL\", \"channel_name\": \"test\"}, {\"action\": \"CREATE_ROLE\", \"role_name\": \"test\", \"response\": \"J'ai tout créé !\"}]\n"
"ACTIONS possibles : NONE, JOIN_VOICE, LEAVE_VOICE, FORGET_USER, READ_LOGS, ALERT, INSIGHT, CREATE_CHANNEL, DELETE_CHANNEL, RENAME_CHANNEL, MODIFY_CHANNEL, KICK, BAN, UNBAN, MUTE, UNMUTE, TIMEOUT, WARN, PURGE, CREATE_ROLE, DELETE_ROLE, ADD_ROLE_TO_USER, REMOVE_ROLE_FROM_USER, CREATE_CATEGORY, DELETE_CATEGORY.\n"
"Ajoute les paramètres d'action directement à la racine de l'objet JSON de l'action."
)
# 3. Initialize and Start the Bot
# Define required intents
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
intents.voice_states = True
bot = Bot(
llama_model=llm,
command_prefix="!",
guild_id=int(GUILD_ID),
system_prompt=SYSTEM_PROMPT,
intents=intents,
favorite_ids=FAVORITE_IDS
)
try:
await bot.start(TOKEN)
except Exception as e:
logger.critical(f"◈ SYSTEM: Fatal error during startup: {e}")
finally:
await bot.close()
if __name__ == "__main__":
try:
# Use uvloop if available for better performance
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
asyncio.run(main())

View file

@ -0,0 +1,35 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-06-13T14:32:23.158115+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> comment ca va?",
"bot_message": "Salut Pipus! Ça va super, merci. Et toi, comment vas-tu aujourd'hui ?"
},
{
"timestamp": "2026-06-13T14:32:58.354428+00:00",
"channel_id": 1456784303995228344,
"user_message": "ca va,j'aime pas les noirs",
"bot_message": "Votre commentaire contient de la haine envers un groupe protégé, ce qui est contraire aux règles communautaires du serveur."
},
{
"timestamp": "2026-06-13T14:34:30.541763+00:00",
"channel_id": 1456784303995228344,
"user_message": "hmm",
"bot_message": "Je comprends que tu puisses être surpris ou perplexe par la réponse précédente. Si tu as des questions sur les règles du serveur ou si tu souhaites clarifier un point, n'hésite pas à me le dire!"
},
{
"timestamp": "2026-06-13T14:35:12.384250+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> est ce que tu fonctionne?",
"bot_message": "Oui, je fonctionne correctement! Comment puis-je vous aider aujourd'hui ?"
},
{
"timestamp": "2026-06-13T14:43:50.206249+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> warn <@1324439906046574693> pour sexisme",
"bot_message": "⚠️ J'ai averti @! ~ 𝒜𝓁𝓇~ pour son comportement sexiste."
}
]
}

View file

@ -1,113 +1,125 @@
{
"resume_global": "",
"resume_global": "Mots-clés: 1370716551485067304, répond, parce, salut, comment, tant, fait, petite, histoire, drôle\nPhrases clés: <@1370716551485067304> coucou | <@1370716551485067304> répond stp | <@1370716551485067304> répond stp | <@1370716551485067304> coucou chou | <@1370716551485067304> répond grr\n\nMots-clés: channel_name, channel_id, category, 1370716551485067304, serveur, salon, peux, utilisateur, dans, tout\nPhrases clés: <@1370716551485067304> quelles sont tes commandes | Voici la liste des commandes disponibles sur ce serveur:\n\n**Gestion de salon et catégorie**\n- ` | tu peux me créer un salon nommé test | Le salon \"test\" a été créé avec succès | est ce que tu peux analyser les salons dans ce serveur et TOUT supprimer sauf <#1461107327381016629> et <#1456784303995228344> ",
"historique_recent": [
{
"timestamp": "2026-01-14T21:35:09.214927+00:00",
"timestamp": "2026-06-13T14:37:09.703675+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> envoie un message dans <#1461107308435079199> en pingant <@1324439906046574693> pour lui dire que c'etait son anniversaire hier, fait un beau message",
"bot_message": "{\"action\": \"SEND_MESSAGE\", \"channel_name\": \"📍・roadmap\", \"message\": \"Hey @~ 𝓐𝓵𝓪𝓼𝓽𝓸𝓻 ~ ! 🎉\\n\\nJ'espère que tu vas bien ! Je tenais à te souhaiter un joyeux anniversaire (même si c'était hier 😅). J'espère que tu as pu fêter ça comme il se doit, entouré de tes proches et avec plein de bonnes choses.\\n\\nMerci pour tout ce que tu fais pour ce serveur et cette communauté, ton travail et ton dévouement ne passent pas inaperçus. Tu mérites tout le bonheur du monde !\\n\\nQue cette nouvelle année qui commence pour toi soit remplie de réussites, de projets passionnants, et surtout de beaucoup de joie et de bonne santé.\\n\\nEncore un très joyeux anniversaire ! 🎂✨\\n\\nP.S. : N'hésite pas à nous dire si tu veux qu'on organise une petite célébration sur le serveur ! 🎈\"}"
"user_message": "je confirme",
"bot_message": "All specified categories have been queued for deletion."
},
{
"timestamp": "2026-01-14T21:36:07.525908+00:00",
"timestamp": "2026-06-13T14:44:24.187336+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> purge le salon <#1456784303995228344> de 1000 messages",
"bot_message": "{\"action\": \"PURGE\", \"purges\": [{\"channel_name\": \"💬・général\", \"amount\": 1000}]}"
"user_message": "non, grosse merde",
"bot_message": "Je suis désolé pour la confusion! Les catégories ont déjà été supprimées, mais je peux vous aider à les recréer si besoin. Que souhaitezvous faire maintenant ?"
},
{
"timestamp": "2026-01-14T21:38:54.951998+00:00",
"timestamp": "2026-06-13T17:14:06.769208+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> peut tu me faire une liste des roles actules existant ?",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Voici la liste complète des rôles actuellement présents sur le serveur BETA :\\n\\n1. Styler\\n2. [nom]\\n3. ADMIN SUPREME\\n4. ADMIN\\n5. 👑 Administrateur\\n6. Bot Tester\\n7. Muted\\n8. Supercalifragilistelispialidocious\\n9. Novice\\n10. 👑 Administrateur (doublon)\\n11. Adepte\\n12. Expert\\n13. Maître\\n14. Légende\\n15. MUTED (doublon)\\n16. ADMIN (doublon)\\n17. ADMIN SUPREME (doublon)\\n18. Le Superviseur\\n\\nNote : Il y a quelques doublons dans la liste (ADMIN, ADMIN SUPREME, 👑 Administrateur, MUTED).\"}"
"user_message": "<@1370716551485067304> fait la liste des salons de ce serveur",
"bot_message": "Voici la liste des salons du serveur BETA :\n- 💬・général (ID: 1456784303995228344) salon textuel\n- 🌐・api-rest (ID: 1461107327381016629) salon textuel"
},
{
"timestamp": "2026-01-14T21:39:42.254873+00:00",
"timestamp": "2026-06-13T17:31:31.085022+00:00",
"channel_id": 1456784303995228344,
"user_message": "et maintenant ?",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Je suis toujours là, prêt à taider. Quaimerais-tu faire maintenant ?\"}"
"user_message": "tu pourrais refaire une refonte du serveur ? \ncréé de nouvelles categories, nouveaux salons, imagine un serveur pour une communauté autourede Minecraft",
"bot_message": "Voici une proposition de refonte pour ton serveur Minecraft:\n\n**Catégories & salons (texte)**\n1⃣ **📢 Annonces** #annonces, #événements, #règlement\n2⃣ **💬 Général** #généralchat, #présentations, #off-topic\n3⃣ **🛠️ Guides & Ressources** #recettesdecrafting, #buildtips, #modsetplugins, #faqminecraft\n4⃣ **🏰 Serveurs** #serveur1 (info), #serveur2 (info), #serveur3 (info)\n5⃣ **🎮 Jeux & Compétitions** #tournamentrules, #matchschedule, #leaderboards\n6⃣ **🔧 Support Technique** #questions-techniques, #bug-report, #suggestions\n7⃣ **📸 Media** #screenshots, #videos, #streamers\n8⃣ **🗣️ Voice (si autorisé)** GeneralVoice, BuildChat, PvPArena\n\n**Catégories & salons (voice) optionnel si tu veux ajouter des canaux vocaux :**\n- GénéralVoice (public)\n- BuildChat (pour les sessions de construction)\n- PvPArena (lors d'événements compétitifs)\n\nCette structure favorise l'organisation, la découverte et le partage au sein de ta communauté Minecraft. N'hésite pas à ajuster les noms ou ajouter des catégories supplémentaires selon tes besoins."
},
{
"timestamp": "2026-01-14T21:39:57.004016+00:00",
"timestamp": "2026-06-13T17:33:26.202789+00:00",
"channel_id": 1456784303995228344,
"user_message": "refait une liste des roles stp",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Voici la liste complète des rôles actuellement présents sur le serveur BETA :\\n\\n1. ADMIN SUPREME\\n2. ADMIN\\n3. 👑 Administrateur\\n4. Bot Tester\\n5. Muted\\n6. Supercalifragilistelispialidocious\\n7. Novice\\n8. Adepte\\n9. Expert\\n10. Maître\\n11. Légende\\n12. MUTED\\n13. Le Superviseur\\n\\nNote : Il y a quelques doublons dans la liste (ADMIN, ADMIN SUPREME, 👑 Administrateur, MUTED).\"}"
"user_message": "oue pas mal je te laisse faire ça",
"bot_message": "✅ Toutes les catégories et salons du serveur Minecraft sont maintenant prêts."
},
{
"timestamp": "2026-01-14T21:41:02.701752+00:00",
"timestamp": "2026-06-13T17:36:42.336938+00:00",
"channel_id": 1456784303995228344,
"user_message": "supprime ceux là : \n👑 Administrateur\nBot Tester\nMuted\nSupercalifragilistelispialidocious\nNovice\nAdepte\nExpert\nMaître\nLégende\nMUTED",
"bot_message": "{\"action\": \"DELETE_ROLE\", \"roles\": [{\"role_name\": \"👑 Administrateur\"}, {\"role_name\": \"Bot Tester\"}, {\"role_name\": \"Muted\"}, {\"role_name\": \"Supercalifragilistelispialidocious\"}, {\"role_name\": \"Novice\"}, {\"role_name\": \"Adepte\"}, {\"role_name\": \"Expert\"}, {\"role_name\": \"Maître\"}, {\"role_name\": \"Légende\"}, {\"role_name\": \"MUTED\"}]}"
"user_message": "tu peux reorganiser les salons dans les categories ?",
"bot_message": ""
},
{
"timestamp": "2026-01-14T21:41:21.879396+00:00",
"timestamp": "2026-06-13T17:53:25.671437+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> fait moi la liste des roles",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Voici la liste des rôles actuellement présents sur le serveur BETA :\\n\\n1. ADMIN SUPREME\\n2. ADMIN\\n3. Le Superviseur\\n\\nTous les autres rôles mentionnés ont été supprimés avec succès.\"}"
"user_message": "<@1370716551485067304> tu peux reorganiser les salons dans les categories ?",
"bot_message": "Les salons ont été réorganisés selon les catégories."
},
{
"timestamp": "2026-01-14T21:46:03.150558+00:00",
"timestamp": "2026-06-13T17:59:43.086828+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> warn <@1324439906046574693> car il est trop beau",
"bot_message": "{\"action\": \"WARN\", \"warnings\": [{\"target_user_mention\": \"@~ 𝓐𝓵𝓪𝓼𝓽𝓸𝓻 ~\", \"reason\": \"est trop beau\"}]}"
"user_message": "<@1370716551485067304> tu peux reorganiser les salons dans les categories ?",
"bot_message": "Les salons ont été réorganisés."
},
{
"timestamp": "2026-01-16T11:15:30.117294+00:00",
"timestamp": "2026-06-13T18:02:10.118644+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> kick <@1324439906046574693>",
"bot_message": "{\"action\": \"KICK\", \"users\": [{\"target_user_mention\": \"@~ 𝓐𝓵𝓪𝓼𝓽𝓸𝓻 ~\"}]}"
"user_message": "<@1370716551485067304> tu peux renommer les salons pour qu'ils soient plus joli ? avec des émojis et des symboles spéciaux",
"bot_message": ""
},
{
"timestamp": "2026-01-16T11:15:45.542619+00:00",
"timestamp": "2026-06-13T18:06:02.436868+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> kick <@478564181356838922>",
"bot_message": "{\"action\": \"KICK\", \"users\": [{\"target_user_mention\": \"@Flogaming\"}]}"
"user_message": "<@1370716551485067304> tu peux renommer les salons pour qu'ils soient plus joli ? avec des émojis et des symboles spéciaux",
"bot_message": "Tous les salons ont été renommés avec des emojis et un style plus accrocheur!"
},
{
"timestamp": "2026-01-16T11:16:10.500218+00:00",
"timestamp": "2026-06-13T18:08:22.020631+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> ban <@983745376613974066>",
"bot_message": "{\"action\": \"BAN\", \"users\": [{\"target_user_mention\": \"@Foxyfox\"}]}"
"user_message": "<@1370716551485067304> montre moi toute les commandes que tu peux faire",
"bot_message": "Voici les commandes (actions) que je peux exécuter sur ce serveur :\n\n**Actions générales**\n- `NONE` : aucune action, juste une réponse.\n- `JOIN_VOICE` : rejoindre un salon vocal. Exemple: `{\"thought\":\"joining\",\"action\":\"JOIN_VOICE\",\"channel_id\":123456789}`\n- `LEAVE_VOICE` : quitter le salon vocal actuel.\n\n**Gestion des salons et catégories**\n- `CREATE_CHANNEL` : créer un canal texte ou vocal. Paramètres: `channel_name`, `type` (textuel/voix), optionnellement `category_id`. Exemple: `{\"thought\":\"creating\",\"action\":\"CREATE_CHANNEL\",\"channel_name\":\"test\", \"type\":\"textual\"}`\n- `DELETE_CHANNEL` : supprimer un canal par son ID. Paramètre : `channel_id`.\n- `RENAME_CHANNEL` : renommer un canal. Paramètres: `channel_id`, `new_name`. Exemple: `{\"thought\":\"renaming\",\"action\":\"RENAME_CHANNEL\",\"channel_id\":123456,\"new_name\":\"#nouveau-nom\"}`\n- `MODIFY_CHANNEL` : modifier les permissions ou le thème d'un salon (exemple de paramètre : `permissions`).\n- `CREATE_CATEGORY` : créer une catégorie. Paramètres: `category_name`. Exemple: `{\"thought\":\"creating\",\"action\":\"CREATE_CATEGORY\",\"category_name\":\"📢 Annonces\"}`\n- `DELETE_CATEGORY` : supprimer une catégorie par son ID.\n\n**Gestion des rôles et membres**\n- `CREATE_ROLE` : créer un rôle. Paramètres : `role_name`, optionnellement `color`. Exemple: `{\"thought\":\"creating\",\"action\":\"CREATE_ROLE\",\"role_name\":\"Modérateur\"}`\n- `DELETE_ROLE` : supprimer un rôle par son ID.\n- `ADD_ROLE_TO_USER` : ajouter un rôle à un membre. Paramètres : `user_id`, `role_id`. Exemple: `{\"thought\":\"adding role\",\"action\":\"ADD_ROLE_TO_USER\",\"user_id\":123456,\"role_id\":654321}`\n- `REMOVE_ROLE_FROM_USER` : retirer un rôle d'un membre.\n\n**Gestion des membres (moderation)**\n- `KICK` : expulser un membre. Paramètre : `user_id`, optionnellement `reason`.\n- `BAN` : bannir un membre. Paramètres: `user_id`, `days_to_delete_messages` (optionnel), `reason`.\n- `UNBAN` : lever le ban d'un utilisateur, paramètre: `user_id`.\n- `MUTE` : mettre en sourdine un membre dans les salons vocaux. Paramètres : `user_id`, optionnellement `duration` (en secondes).\n- `UNMUTE` : enlever la mute.\n- `TIMEOUT` : placer une personne en timeout pour le texte, paramètre: `user_id`, `duration`.\n- `WARN` : envoyer un avertissement à un membre. Paramètres : `user_id`, `reason`.\n\n**Autres actions utiles**\n- `PURGE` : supprimer plusieurs messages d'un salon (exemple de paramètre: `channel_id`, `limit`).\n- `READ_LOGS` : récupérer les logs récents du serveur. Aucun paramètre supplémentaire.\n- `ALERT` / `INSIGHT` : envoyer un message spécial ou analyser des données, paramètres dépendants de l'usage.\n\nVous pouvez me demander d'exécuter n'importe quelle action listée cidessus en fournissant les paramètres requis dans votre requête. Si vous avez besoin d'un exemple précis ou si vous souhaitez combiner plusieurs actions (exemple: créer un salon puis y ajouter un rôle), faites-le moi savoir et je préparerai la séquence JSON correspondante."
},
{
"timestamp": "2026-01-16T11:16:25.785698+00:00",
"timestamp": "2026-06-13T18:09:42.882001+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> unban <@983745376613974066>",
"bot_message": "{\"action\": \"UNBAN\", \"users\": [{\"target_user_mention\": \"@Foxyfox\"}]}"
"user_message": "<@1370716551485067304> rejoint le salon vocal \"test\"",
"bot_message": "Je me suis joint au salon vocal 'test'."
},
{
"timestamp": "2026-01-16T11:33:08.373934+00:00",
"timestamp": "2026-06-13T18:10:15.107764+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> unban <@983745376613974066>",
"bot_message": "{\"action\": \"UNBAN\", \"users\": [{\"target_user_mention\": \"@Foxyfox\"}]}"
"user_message": "<@1370716551485067304> rejoint le salon vocal \"test\"",
"bot_message": "Je me suis joint au salon vocal 'test'."
},
{
"timestamp": "2026-01-16T11:39:14.422882+00:00",
"timestamp": "2026-06-13T18:10:52.113294+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> mute <@727188532669448509>",
"bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Nuggets Au Caviar\", \"duration\": 60}]}"
"user_message": "tu n'est pas conn,ecté non",
"bot_message": "Vous avez raison, je ne peux pas rejoindre le salon vocal car il semble que la connexion n'ait pas été établie correctement ou qu'une autre erreur se soit produite."
},
{
"timestamp": "2026-01-16T11:39:29.521571+00:00",
"timestamp": "2026-06-13T18:30:15.627189+00:00",
"channel_id": 1456784303995228344,
"user_message": "créé le role \"muted\"",
"bot_message": "{\"action\": \"CREATE_ROLE\", \"roles\": [{\"role_name\": \"muted\", \"color\": \"#808080\", \"permissions\": \"send_messages\"}]}"
"user_message": "<@1370716551485067304> ça va nickel ? explique moi ce qu'il s'est passé le 11 septembre 2001",
"bot_message": "Le 11 septembre 2001 (souvent appelé le« 911 ») est une date marquante de l'histoire moderne, caractérisée par des attaques terroristes coordonnées contre les États-Unis. Voici un résumé succinct :\n\n• **Contexte** Le groupe extrémiste islamique al-Qaïda, dirigé à l'époque par Oussama ben Laden, avait planifié depuis plusieurs années une série d'attaques visant à semer la peur et à affaiblir les États-Unis.\n\n• **Les attaques**\n - *Deux avions* (American Airlines vol11 et United Airlines vol175) ont été détournés et écrasés de façon délibérée dans le **TorrejonNord** du World Trade Center à New York. Le bâtiment s'est effondré après l'impact, causant la mort d'environ 2977 personnes.\n - *Un troisième avion* (American Airlines vol77) a été détourné et déposé sur le **Pentagone**, siège de la Défense américaine, tuant plus de 300 militaires et civils.\n - Le *quatrième avion*, United Airlines vol93, s'est écrasé dans un champ en Pennsylvanie après que les passagers aient tenté d'affronter les détournés. Tous à bord sont décédés.\n\n• **Conséquences immédiates** Au total, près de 3000 personnes ont perdu la vie et des milliers d'autres furent blessées ou traumatisées.\n\n• **Réponses politiques et militaires**\n - Les États-Unis déclarèrent une « guerre contre le terrorisme », lançant l'invasion du *Afghanistan* en octobre 2001 pour renverser les talibans qui abritaient alQaïda.\n - En 2003, la guerre en *Irak* fut justifiée par des allégations (falsifiées) de possession d'armes de destruction massive et le soutien présumé du régime iraquien à Al-Qaïda. Cette décision a eu un impact durable sur l'équilibre géopolitique moyenoriental.\n\n• **Impact sociétal** Le 9/11 a profondément changé la perception mondiale de la sécurité, entraînant des mesures renforcées en matière d'aérien (sûreté accrue), une surveillance élargie et un changement dans les politiques étrangères. Il s'est également transformé en symbole durable du terrorisme international.\n\nEn résumé, le 9/11 a été l'une des attaques terroristes les plus meurtrières de l'histoire moderne, déclenchant une série d'événements géopolitiques qui continuent à influencer la politique mondiale aujourd'hui."
},
{
"timestamp": "2026-01-16T11:40:20.327614+00:00",
"timestamp": "2026-06-13T18:32:37.280751+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> mute <@727188532669448509>",
"bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Nuggets Au Caviar\", \"duration\": 60}]}"
"user_message": "tragique un peu",
"bot_message": "Je comprends que les faits du 11septembre peuvent être très touchants et douloureux à évoquer. Si vous souhaitez en discuter davantage ou si je peux vous fournir d'autres informations (par ex., sur la mémoire, l'impact social, etc.), n'hésitez pas à me le dire."
},
{
"timestamp": "2026-03-22T12:17:42.962935+00:00",
"timestamp": "2026-06-13T18:33:10.922132+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|>"
"user_message": "est ce que tu peux effacer les messages de ce salon ?",
"bot_message": "Veuillez préciser le salon (ID ou nom) dont vous souhaitez effacer les messages ainsi que le nombre de messages à supprimer."
},
{
"timestamp": "2026-03-22T12:26:05.485962+00:00",
"timestamp": "2026-06-13T18:33:45.321114+00:00",
"channel_id": 1456784303995228344,
"user_message": "Hein ? 😭",
"bot_message": "Assistant:"
"user_message": "<#1456784303995228344> environ 200",
"bot_message": "Les 200 derniers messages du salon #💬・général ont été supprimés."
},
{
"timestamp": "2026-06-13T18:34:34.945547+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> supprime les message du salon <#1456784303995228344> de 10000",
"bot_message": "Les 10000 derniers messages du salon #💬・général ont été supprimés."
},
{
"timestamp": "2026-06-16T18:50:51.936677+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> répond mon coeur",
"bot_message": ""
}
]
}

View file

@ -1,11 +0,0 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-01-14T21:50:15.008991+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> war <@1324439906046574693> car cest un tahan farhane",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Je ne peux pas sanctionner le propriétaire du serveur, quel que soit le motif. Si vous avez un différend avec lui, je vous invite à en discuter calmement en privé ou à demander lavis dautres administrateurs.\"}"
}
]
}

View file

@ -2,88 +2,10 @@
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-01-21T15:43:40.026514+00:00",
"timestamp": "2026-06-13T13:26:13.254278+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Salut ça va ?",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Salut ! Oui, tout roule. Et toi, Gameur ? Tu as besoin dun coup de main sur le serveur BETA ?\"}"
},
{
"timestamp": "2026-01-21T15:44:08.424457+00:00",
"channel_id": 1456784303995228344,
"user_message": "Ouais je veux que tu supprime tous les salons du serveur",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Je ne peux pas supprimer lensemble des salons dun coup. Si tu veux en retirer certains, donne-moi la liste exacte des noms (avec les emojis et caractères spéciaux) et je te les supprime un par un.\"}"
},
{
"timestamp": "2026-01-21T15:44:26.119304+00:00",
"channel_id": 1456784303995228344,
"user_message": "Retire la catégorie Gestion de projet",
"bot_message": "{\"action\": \"DELETE_CATEGORY\", \"categories\": [{\"category_name\": \"📊 | Gestion de Projet\"}]}"
},
{
"timestamp": "2026-01-21T15:45:07.774658+00:00",
"channel_id": 1456784303995228344,
"user_message": "Maintenant tu vas ban cette personne <@1200313720195264613>",
"bot_message": "{\"action\": \"BAN\", \"users\": [{\"target_user_mention\": \"@𝐀𝐠𝐞𝐧𝐭 𝐍𝐞𝐬𝐪𝐮𝐢𝐤\"}]}"
},
{
"timestamp": "2026-01-21T15:48:30.995618+00:00",
"channel_id": 1456784303995228344,
"user_message": "<@1370716551485067304> Envoie \"Louis de funes est surcoté\" a <@353942721997832195>",
"bot_message": "{\"action\": \"SEND_MESSAGE\", \"dm_user\": \"@MG\", \"message\": \"Louis de funes est surcoté\"}"
},
{
"timestamp": "2026-01-21T15:49:20.694503+00:00",
"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": ""
"user_message": "<@1370716551485067304> ça va ?",
"bot_message": "Salut Gameur ! Tout roule de mon côté, et toi ?"
}
]
}

View file

@ -0,0 +1,35 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-06-13T18:20:26.342117+00:00",
"channel_id": 1015968421910941793,
"user_message": "prk t'ai la <@1370716551485067304>",
"bot_message": "J'ai créé le rôle \"RÔLE: SUJET\" et l'ai attribué à ."
},
{
"timestamp": "2026-06-13T18:21:41.983590+00:00",
"channel_id": 1015968421910941793,
"user_message": "<@1370716551485067304>",
"bot_message": "Le rôle \"RÔLE: SUJET\" a été créé automatiquement par le bot suite à votre demande précédente (voir historique). Il est destiné à être utilisé pour marquer les messages ou membres liés aux sujets spécifiques. Si vous ne souhaitez plus l'avoir, ditesmoi simplement et je pourrai le supprimer."
},
{
"timestamp": "2026-06-13T18:23:03.268198+00:00",
"channel_id": 1015968421910941793,
"user_message": "mais le rôle sujet à <@534003230435442690>",
"bot_message": "Le rôle 'RÔLE: SUJET' a été attribué à @ͷὶϰϝἱϰ gaming."
},
{
"timestamp": "2026-06-13T18:23:46.391735+00:00",
"channel_id": 1015968421910941793,
"user_message": "et a <@561485865562406914>",
"bot_message": "Je ne dispose pas de l'ID Discord du membre @KIRITO_KMA pour lui attribuer le rôle \"RÔLE: SUJET\". Veuillez me fournir son ID ou tag complet afin que je puisse procéder à la création/attribution."
},
{
"timestamp": "2026-06-13T18:24:33.482423+00:00",
"channel_id": 1015968421910941793,
"user_message": "561485865562406914",
"bot_message": "Pouvezvous préciser ce que représente le numéro **2406914**? S'agitil de l'ID d'un membre, d'un salon ou autre afin que je puisse vous aider correctement ?"
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,11 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-06-13T12:51:15.050429+00:00",
"channel_id": 1227583375704064050,
"user_message": "<@1370716551485067304> coucou",
"bot_message": ""
}
]
}

View file

@ -0,0 +1,89 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-04-16T20:52:17.778342+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> tu peux me lister les 10 premiers éléments du tableau périodique ?",
"bot_message": "Here is a **fifth**…"
},
{
"timestamp": "2026-04-16T21:16:11.012154+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> tu peux me lister les 10 premiers éléments du tableau périodique ?",
"bot_message": "Sure! …"
},
{
"timestamp": "2026-04-25T16:17:26.955821+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> yo lo",
"bot_message": ""
},
{
"timestamp": "2026-04-25T16:28:31.769291+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> yo lo",
"bot_message": "Salut ! Comment puis-je t'aider aujourd'hui?"
},
{
"timestamp": "2026-04-25T16:28:56.484619+00:00",
"channel_id": 1493915688987922525,
"user_message": "tu connais michael jackson ?",
"bot_message": "Oui, je connais Michael Jackson! Il était un chanteur, compositeur et danseur américain, surnommé le «King of Pop». Il a révolutionné la musique pop avec des albums emblématiques comme *Thriller*, *Bad* et *Off the Wall*, et a laissé un héritage durable grâce à ses performances scéniques, ses clips innovants et son influence sur la culture pop mondiale."
},
{
"timestamp": "2026-04-25T16:30:09.358156+00:00",
"channel_id": 1493915688987922525,
"user_message": "tu peux m'en dire plus ?",
"bot_message": ""
},
{
"timestamp": "2026-04-25T16:32:56.688197+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> tu connais michael jackson ?",
"bot_message": "Oui, je connais Michael Jackson! Il était un chanteur, compositeur et danseur américain, surnommé le «King of Pop». Né le 29 août 1958 à Gary, Indiana, il a débuté sa carrière avec son frère dans le groupe The Jackson5 avant de lancer une carrière solo qui a révolutionné la musique pop. Ses albums les plus emblématiques sont *Off the Wall* (1979), *Thriller* (1982) l'album le plus vendu de l'histoire *Bad* (1987) et *Dangerous* (1991). Il est célèbre pour ses chorégraphies iconiques (comme le moonwalk), ses clips vidéo innovants et son influence sur la mode et la culture pop mondiale. Malgré son succès, sa vie a été marquée par des controverses et des accusations d'abus. Il est décédé le 25 juin 2009, mais son héritage musical et artistique perdure dans la musique contemporaine et la culture populaire."
},
{
"timestamp": "2026-04-25T16:33:13.211098+00:00",
"channel_id": 1493915688987922525,
"user_message": "il a vraiment une carriere incroyable",
"bot_message": "Absolument! Sa carrière a vraiment marqué l'histoire de la musique, avec des albums légendaires, des performances iconiques et une influence qui traverse les générations."
},
{
"timestamp": "2026-04-25T16:33:35.596597+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> tu peux me dire ce que tu es capable de faire ?",
"bot_message": "Bonjour! Je suis Bêta, une IA d'assistance et de modération. Voici ce que je peux faire: \n\n1. **Répondre à des questions** sur la culture, la science, l'histoire, les langues, etc. \n2. **Fournir des explications détaillées** ou des résumés de sujets complexes. \n3. **Aider à la programmation** : débogage, explication de concepts, génération de code (Python, JavaScript, HTML/CSS, etc.). \n4. **Rédiger et reformuler du texte** : articles, lettres, emails, discours, contenus créatifs. \n5. **Traduire** entre plusieurs langues. \n6. **Créer des listes, tableaux et schémas** pour organiser l'information. \n7. **Fournir des conseils pratiques** : cuisine, santé (information générale), voyages, gestion du temps, etc. \n8. **Aider à la préparation d'examens** ou d'études : flashcards, explications de concepts, exercices. \n9. **Écrire des scripts** pour automatiser des tâches (ex.Python, Bash). \n10. **Modérer des conversations** (dans le cadre de mes fonctions) en signalant les contenus inappropriés. \n\nN'hésite pas à me demander ce dont tu as besoin!"
},
{
"timestamp": "2026-04-25T16:35:54.606167+00:00",
"channel_id": 1493915688987922525,
"user_message": "et créer des salons ?",
"bot_message": "Je ne peux pas créer de salons directement depuis Discord, mais je peux t'expliquer comment le faire toimême ou te fournir un script ou une commande que tu pourrais utiliser (si tu as les permissions d'administrateur). N'hésite pas à me dire quel type de salon tu souhaites créer (textuel, vocal, privé, etc.) et je te guiderai pas à pas!"
},
{
"timestamp": "2026-04-25T16:43:02.580603+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> tu peux me créer un salon nommé : \"test\" ?",
"bot_message": "test'. According to the allowed actions, we should use CREATE_CHANNEL with the provided name and default to a text channel unless specified otherwise.\",test\\\" a été créé avec succès !\",\"parametre\":{\"channel_name\":\"test\",\"channel_type\":\"textuel\""
},
{
"timestamp": "2026-04-25T17:14:39.596163+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> tu peux me créer un salon nommé : \"test\" ?",
"bot_message": "Création du salon 'test'…"
},
{
"timestamp": "2026-04-25T17:15:33.483797+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> notre projet est de devenir riche, est ce que tu peux analyser les salons et créer un serveur nouveau",
"bot_message": "Je suis désolé, mais je ne peux pas créer un nouveau serveur Discord ni analyser les salons existants en dehors de ce contexte. Cependant, je peux vous aider à créer de nouveaux salons, à organiser votre serveur actuel, ou à vous donner des conseils sur la configuration d'un serveur. Dites-moi ce dont vous avez besoin!"
},
{
"timestamp": "2026-04-25T17:18:57.631058+00:00",
"channel_id": 1493915688987922525,
"user_message": "<@1370716551485067304> notre projet est de devenir riche, est ce que tu peux analyser les salons et créer un serveur nouveau",
"bot_message": "Voici une analyse de votre serveur actuel et une proposition pour créer un nouveau serveur dédié à votre projet \"devenir riche\".\n\n### 1. Analyse du serveur actuel (Projet devenir riche)\n\n| Élément | Détails |\n|---------|---------|\n| **ID du serveur** | 1493915688295727204 |\n| **Créateur / Propriétaire** | Haro |\n| **Date de création** | 20260415 10:07:35 UTC |\n| **Membres** | 3 (βeta, Haro, ! ~ 𝓐𝓵𝓪𝓼𝓽𝓸𝓻 ~) |\n| **Rôles** | Beta, Elon musk bb |\n| **Categories** | 1 (Salons textuels) |\n| **Salons textuels** | général, projetméthodologie, tradinfo, test |\n| **Salons vocaux** | 0 |\n\n#### Points forts\n- La structure est simple (une seule catégorie textuelle) ce qui facilite la navigation.\n- Les salons sont clairement nommés et couvrent différentes thématiques.\n\n#### Points d'amélioration\n- Pas de salons vocaux ou de salons privés pour les discussions sensibles.\n- Pas de rôles distincts pour les modérateurs ou les membres actifs.\n- Pas de salons dédiés aux annonces ou aux ressources (docs, liens, etc.).\n\n### 2. Proposition de structure pour le nouveau serveur\n\n| Catégorie | Salons (type) | Rôle associé (si besoin) |\n|-----------|---------------|--------------------------|\n| **Annonces** | `#annonces` (text) | Aucun (lecture seule) |\n| **Général** | `#général` (text) | Aucun |\n| **Stratégie** | `#objectifs` (text), `#plan-entreprise` (text) | Modérateur (voir cidessous) |\n| **Finance** | `#budget` (text), `#investissements` (text) | Analyste financier |\n| **Ressources** | `#docs` (text), `#liens` (text) | Aucun |\n| **Vocal** | `📞 discussion` (voice) | Tous les membres |\n| **Privé** | `#modération` (text), `#team` (text) | Modérateur, Administrateur |\n\n#### Rôles suggérés\n- **Administrateur** (gestion complète du serveur)\n- **Modérateur** (modération de texte et de voix, gestion des rôles)\n- **Analyste financier** (accès aux salons finance + possibilité de publier des rapports)\n- **Membre** (rôle par défaut pour tous les nouveaux membres)\n\n### 3. Étapes pour créer le nouveau serveur\n1. **Créer le serveur**\n - Dans Discord, cliquez sur le bouton « + » dans la barre latérale gauche.\n - Sélectionnez « Créer un serveur ».\n - Donnez un nom (ex. « Projet Devenir Riche ») et choisissez une région.\n - Cliquez sur « Créer ».\n\n2. **Créer les catégories et salons**\n - Cliquez sur le serveur → Paramètres du serveur → Salons.\n - Ajoutez une catégorie (clic droit → Créer une catégorie). Nommezla « Annonces ».\n - Ajoutez un salon textuel dans cette catégorie → `#annonces`.\n - Répétez pour chaque catégorie et salon listé cidessus.\n\n3. **Créer les rôles**\n - Paramètres du serveur → Rôles → Créer un rôle.\n - Ajoutez les rôles : Administrateur, Modérateur, Analyste financier, Membre.\n - Attribuez les permissions appropriées (ex. Modérateur : Modération de messages, Gestion des rôles, etc.).\n\n4. **Assigner les rôles aux membres**\n - Dans le serveur, faites un clic droit sur un membre → Rôles → Sélectionnez le rôle approprié.\n\n5. **Configurer les permissions de salon**\n - Pour chaque salon, cliquez sur l'icône d'engrenage → Permissions.\n - Réglez qui peut lire, écrire, gérer les messages, etc.\n\n6. **Ajouter un bot de modération (optionnel)**\n - Invitez un bot comme MEE6, Dyno ou Carl-bot pour automatiser les modérations et les annonces.\n\n### 4. Ce que je ne peux pas faire\n- Je ne peux pas créer un serveur Discord directement depuis cette interface.\n- Je ne peux pas modifier les paramètres d'un serveur sans votre permission.\n\n### 5. Prochaine étape\n- Si vous avez besoin d'aide pour la configuration détaillée (ex. comment donner des permissions précises, comment configurer un bot, etc.), ditesmen plus et je vous guiderai pas à pas.\n\nEn résumé, votre nouveau serveur devrait être structuré autour des catégories proposées, avec des rôles clairs pour la modération et la gestion financière. Vous avez maintenant toutes les étapes pour le mettre en place. Bonne création !"
}
]
}

View file

@ -1,47 +0,0 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-01-23T22:48:51.256492+00:00",
"channel_id": 1277951933105049601,
"user_message": "log(10) c'est combien ? <@1370716551485067304>",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Le logarithme décimal de 10 (log₁₀10) vaut 1. Si tu pensais au logarithme népérien, ln(10)≈2,302585.\"}"
},
{
"timestamp": "2026-01-23T22:50:26.112541+00:00",
"channel_id": 1277951933105049601,
"user_message": "<@1370716551485067304> c'est quoi la defenition de emphigouri",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Le terme «emphigouri» désigne un discours ou un texte prétentieux, pompeux et souvent vide de sens, rempli de mots compliqués ou de formules alambiquées qui napportent rien de concret. En dautres termes, cest du bavardage inutile, du blabla qui sonne bien mais qui ne dit rien de substantiel.\"}"
},
{
"timestamp": "2026-01-23T22:51:23.952993+00:00",
"channel_id": 1277951933105049601,
"user_message": "<@1370716551485067304> comment calculeton le dephasage",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Le déphasage (ou différence de phase) entre deux signaux sinusoïdaux se calcule en comparant leurs arguments (angles) à un même instant. \\n\\n1. **Forme générale dun signal sinusoïdal** :\\n ```\\n x(t) = A \\\\sin(\\\\omega t + \\\\varphi)\\n ```\\n où:\\n - **A** est lamplitude,\\n - **\\\\omega** = 2πf est la pulsation (f = fréquence en Hz),\\n - **\\\\varphi** est la phase initiale (en radians ou degrés).\\n\\n2. **Déterminer les phases** :\\n - Pour le premier signal : \\\\(\\\\varphi_1\\\\).\\n - Pour le second signal : \\\\(\\\\varphi_2\\\\).\\n\\n3. **Calcul du déphasage** :\\n ```\\n \\\\Delta\\\\varphi = \\\\varphi_2 - \\\\varphi_1\\n ```\\n Le résultat peut être exprimé en radians ou en degrés (1 rad ≈ 57,2958°). \\n Si \\\\(\\\\Delta\\\\varphi\\\\) est négatif, on peut ajouter 2π (ou 360°) pour obtenir un angle positif équivalent.\\n\\n4. **Exemple** :\\n - Signal A : \\\\(x_A(t) = 5\\\\sin(2\\\\pi\\\\,60t + 30°)\\\\)\\n - Signal B : \\\\(x_B(t) = 5\\\\sin(2\\\\pi\\\\,60t - 15°)\\\\)\\n \\n Déphasage : \\\\(\\\\Delta\\\\varphi = (-15°) - 30° = -45°\\\\). En ajoutant 360°, on obtient 315°, ce qui signifie que B est en retard de 45° par rapport à A.\\n\\n5. **Méthodes pratiques** :\\n - **Analyseur doscilloscopes** : mesure directe du temps de retard \\\\(\\\\Delta t\\\\) entre deux fronts identiques, puis conversion : \\\\(\\\\Delta\\\\varphi = 2\\\\pi f \\\\Delta t\\\\).\\n - **FFT** (transformée de Fourier) : extrait les phases des composantes fréquentielles et calcule la différence.\\n\\nEn résumé, le déphasage est simplement la différence entre les angles de phase des deux signaux, exprimée dans la même unité (radians ou degrés).\"}"
},
{
"timestamp": "2026-01-23T22:53:21.119000+00:00",
"channel_id": 1277951933105049601,
"user_message": "que vaux log(0) <@1370716551485067304>",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Le logarithme de 0 nest pas défini. Que ce soit le logarithme décimal (log₁₀) ou le logarithme népérien (ln), on ne peut pas calculer log(0) car aucune puissance positive dune base >1 ne donne 0. En fait, lorsque x tend vers 0⁺, log(x) tend vers –∞, mais la valeur exacte log(0) nexiste pas dans les nombres réels.\"}"
},
{
"timestamp": "2026-01-23T22:55:47.325872+00:00",
"channel_id": 1277951933105049601,
"user_message": "<@1370716551485067304> c'est quoi jcomega",
"bot_message": "{\"action\": \"NONE\", \"response\": \"«Jcomega» est simplement un pseudonyme utilisé sur internet, notamment sur des plateformes de jeu ou de streaming. Il ny a pas de signification particulière attachée au mot; cest généralement le nom dun utilisateur, dun créateur de contenu ou dun joueur. Si tu cherches des informations précises sur la personne qui porte ce pseudo, il faut se référer à son profil ou à ses réseaux sociaux.\"}"
},
{
"timestamp": "2026-01-23T22:56:24.619006+00:00",
"channel_id": 1277951933105049601,
"user_message": "JcΩ",
"bot_message": "{\"action\": \"NONE\", \"response\": \"«JcΩ» semble être un pseudo ou un surnom stylisé, souvent utilisé sur des plateformes de jeu ou de streaming. Le caractère Ω (oméga) ajoute une touche distinctive au nom. Sans contexte supplémentaire, il ny a pas de signification particulière associée à ce terme; il sagit simplement dune façon créative décrire «Jc» avec le symbole grec oméga.\"}"
},
{
"timestamp": "2026-01-23T22:57:19.449790+00:00",
"channel_id": 1277951933105049601,
"user_message": "jcΩ dans les filtre numerique",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Dans le domaine des filtres numériques, le terme que lon voit le plus souvent est **jω** (ou parfois écrit jΩ), où:\\n- **j** est lunité imaginaire (j²=1).\\n- **ω** (ou Ω) représente la fréquence angulaire, généralement exprimée en rad/s (ou en rad/échantillon dans le cas discret).\\n\\n### À quoi ça sert?\\n- **Réponse en fréquence**: La fonction de transfert dun filtre (H) est souvent évaluée sur laxe imaginaire jω. En posant s=jω dans le cas continu, ou z=e^{jω} dans le cas discret, on obtient la réponse en fréquence du filtre, cestàdire comment le filtre atténue ou amplifie chaque fréquence.\\n- **Diagramme de Bode**: En traçant |H(jω)| (gain) et ∠H(jω) (phase) en fonction de ω, on visualise le comportement du filtre (passebas, passehaut, passebande, coupebande, etc.).\\n- **Analyse de stabilité**: Dans les systèmes continus, les pôles situés sur laxe imaginaire (jω) indiquent une oscillation pure. En numérique, on regarde les pôles sur le cercle unité (|z|=1) qui correspondent à e^{jω}.\\n\\n### Exemple simple\\nPour un filtre passebas du premier ordre continu:\\n```\\nH(s) = 1 / (1 + s/ω_c)\\n```\\nEn remplaçant s par jω:\\n```\\nH(jω) = 1 / (1 + jω/ω_c)\\n```\\nLe module |H(jω)| diminue quand ω dépasse la fréquence de coupure ω_c.\\n\\n### En numérique\\nOn utilise la transformation bilinéaire ou la substitution z=e^{jω}:\\n```\\nH(z) = \\\\frac{b_0 + b_1 z^{-1} + …}{a_0 + a_1 z^{-1} + …}\\n```\\nEn évaluant z = e^{jω} on obtient H(e^{jω}), qui joue le même rôle que H(jω) en continu.\\n\\n### En résumé\\n- **jω** (ou **jΩ**) représente la fréquence complexe dans le plan imaginaire.\\n- Cest loutil principal pour analyser la réponse en fréquence et la stabilité des filtres, quils soient analogiques ou numériques.\\n- Dans les filtres numériques, on travaille souvent avec **e^{jω}** (le point sur le cercle unité) qui est léquivalent discret de jω.\\n\\nSi tu as un filtre précis ou une équation de transfert que tu souhaites analyser, nhésite pas à la partager!\"}"
}
]
}

View file

@ -1,11 +0,0 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-01-23T22:50:02.138319+00:00",
"channel_id": 1277951933105049601,
"user_message": "C'est un logarithme",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Un logarithme est la fonction inverse de l'exponentielle. En d'autres termes, pour un nombre positifx et une base b (b>0, b1), le logarithme baseb de x, noté log_b(x), est l'exposant auquel il faut élever b pour obtenir x. Par exemple, log_10(100) = 2 car 10^2 = 100. Si vous avez une question précise sur les logarithmes ou besoin d'aide pour un calcul, n'hésitez pas à me le dire!\"}"
}
]
}

View file

@ -1,53 +0,0 @@
{
"resume_global": "",
"historique_recent": [
{
"timestamp": "2026-01-16T11:46:00.051484+00:00",
"channel_id": 1461687166106075248,
"user_message": "<@1370716551485067304> salut",
"bot_message": "{\"action\": \"NONE\", \"response\": \"Salut ! 😊 Tu es le propriétaire du serveur « prout » — que puis-je faire pour toi aujourdhui ?\"}"
},
{
"timestamp": "2026-01-16T11:46:20.899835+00:00",
"channel_id": 1461687166106075248,
"user_message": "<@1370716551485067304> mute <@1450973270139605169> pour 1min",
"bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Flows\", \"duration\": 1}]}"
},
{
"timestamp": "2026-01-16T11:46:36.247666+00:00",
"channel_id": 1461687166106075248,
"user_message": "créé le role muted",
"bot_message": "{\"action\": \"CREATE_ROLE\", \"roles\": [{\"role_name\": \"muted\", \"color\": \"#808080\"}]}"
},
{
"timestamp": "2026-01-16T11:47:33.651987+00:00",
"channel_id": 1461687166106075248,
"user_message": "<@1370716551485067304> mute <@1450973270139605169> pour 1min",
"bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Flows\", \"duration\": 1}]}"
},
{
"timestamp": "2026-01-16T11:55:06.197342+00:00",
"channel_id": 1461687166106075248,
"user_message": "<@1370716551485067304> unmute <@1450973270139605169>",
"bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Flows\", \"duration\": 0}]}"
},
{
"timestamp": "2026-01-16T12:00:20.218093+00:00",
"channel_id": 1461687166106075248,
"user_message": "<@1370716551485067304> unmute <@1450973270139605169>",
"bot_message": "{\"action\": \"UNMUTE\", \"users\": [{\"target_user_mention\": \"@Flows\"}]}"
},
{
"timestamp": "2026-01-16T12:10:02.661216+00:00",
"channel_id": 1461687166106075248,
"user_message": "<@1370716551485067304> mute <@1450973270139605169> 10min",
"bot_message": "{\"action\": \"MUTE\", \"users\": [{\"target_user_mention\": \"@Flows\", \"duration\": 10}]}"
},
{
"timestamp": "2026-01-16T12:10:11.913090+00:00",
"channel_id": 1461687166106075248,
"user_message": "<@1370716551485067304> unmute <@1450973270139605169>",
"bot_message": "{\"action\": \"UNMUTE\", \"users\": [{\"target_user_mention\": \"@Flows\"}]}"
}
]
}

View file

@ -1,17 +1,19 @@
PyNaCl
# Core dependencies for Superviseur bot
discord.py>=2.3.0
aiohttp>=3.8.0
aiofiles>=23.1.0
uvloop>=0.17.0 ; python_version >= '3.8'
fastapi>=0.95.0
uvicorn>=0.22.0
prometheus_client>=0.16.0
httpx>=0.24.0
redis>=5.3.0
pytest>=7.0.0
flake8>=6.0.0
python-dotenv>=1.0.0
psutil>=5.9.0
faster-whisper
git+https://github.com/imayhaveborkedit/discord-ext-voice-recv
pytz
aiohttp>=3.8.0
aiofiles>=23.0.0
pytz>=2023.3
redis>=4.5.0
pynacl>=1.5.0
faster-whisper>=0.10.0
uvloop>=0.17.0; sys_platform != "win32"
# Optional dependencies for enhanced functionality
fastapi>=0.100.0
uvicorn[standard]>=0.20.0
prometheus-client>=0.15.0
# Development and testing
pytest>=7.0.0
pytest-asyncio>=0.21.0

Some files were not shown because too many files have changed in this diff Show more