Merge branch 'Eliott' into 'main'
Eliott See merge request Omega_Kube/beta!3
This commit is contained in:
commit
5b71c9fe28
9415 changed files with 3095 additions and 1899580 deletions
18
.env
18
.env
|
|
@ -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
79
.gitignore
vendored
Executable file → Normal 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
160
README.md
Normal 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`
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
743
actions.log
743
actions.log
File diff suppressed because one or more lines are too long
215
beta.py
215
beta.py
|
|
@ -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)
|
||||
165
beta.py.save
165
beta.py.save
|
|
@ -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)
|
||||
|
|
@ -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
16
brain/memory.py
Normal 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
80
brain/moderation.py
Normal 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.
BIN
commandes/autres/__pycache__/config.cpython-313.pyc
Normal file
BIN
commandes/autres/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
commandes/autres/__pycache__/logger.cpython-313.pyc
Normal file
BIN
commandes/autres/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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).")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
BIN
commandes/security/__pycache__/auto_moderation.cpython-313.pyc
Normal file
BIN
commandes/security/__pycache__/auto_moderation.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
7
core/action_executor.py
Normal 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
162
core/action_router.py
Normal 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
624
core/bot.py
Normal 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
37
core/content.py
Normal 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()
|
||||
|
|
@ -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
4
core/context_builder.py
Normal 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
|
||||
|
|
@ -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
158
core/llm.py
Normal 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
83
core/log_parser.py
Normal 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
75
core/response_handler.py
Normal 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
4
core/task_manager.py
Normal 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
166
core/tasks.py
Normal 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())
|
||||
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
|
|
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
98
main.py
Normal file
98
main.py
Normal 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())
|
||||
35
memoires/BETA/1225138254051938397.json
Normal file
35
memoires/BETA/1225138254051938397.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 souhaitez‑vous 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 à t’aider. Qu’aimerais-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éral‑chat, #présentations, #off-topic\n3️⃣ **🛠️ Guides & Ressources** – #recettes‑de‑crafting, #build‑tips, #mods‑et‑plugins, #faq‑minecraft\n4️⃣ **🏰 Serveurs** – #serveur‑1 (info), #serveur‑2 (info), #serveur‑3 (info)\n5️⃣ **🎮 Jeux & Compétitions** – #tournament‑rules, #match‑schedule, #leaderboards\n6️⃣ **🔧 Support Technique** – #questions-techniques, #bug-report, #suggestions\n7️⃣ **📸 Media** – #screenshots, #videos, #streamers\n8️⃣ **🗣️ Voice (si autorisé)** – General‑Voice, Build‑Chat, PvP‑Arena\n\n**Catégories & salons (voice) – optionnel si tu veux ajouter des canaux vocaux :**\n- Général‑Voice (public)\n- Build‑Chat (pour les sessions de construction)\n- PvP‑Arena (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 ci‑dessus 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 « 9‑11 ») 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 vol 11 et United Airlines vol 175) ont été détournés et écrasés de façon délibérée dans le **Torrejon‑Nord** du World Trade Center à New York. Le bâtiment s'est effondré après l'impact, causant la mort d'environ 2 977 personnes.\n - *Un troisième avion* (American Airlines vol 77) 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 vol 93, 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 3 000 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 al‑Qaï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 moyen‑oriental.\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 11 septembre 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 10 000 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 c’est 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 l’avis d’autres administrateurs.\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 d’un 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 l’ensemble des salons d’un 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 ?"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
memoires/Omega_Kube/1133397650184609792.json
Normal file
35
memoires/Omega_Kube/1133397650184609792.json
Normal 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, dites‑moi 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": "Pouvez‑vous préciser ce que représente le numéro **2406914** ? S'agit‑il 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
11
memoires/Perry.Corporation/1324439906046574693.json
Normal file
11
memoires/Perry.Corporation/1324439906046574693.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
89
memoires/Projet_devenir_riche/1324439906046574693.json
Normal file
89
memoires/Projet_devenir_riche/1324439906046574693.json
Normal 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 Jackson 5 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 toi‑mê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** | 2026‑04‑15 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, projet‑méthodologie, trad‑info, 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 ci‑dessous) |\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). Nommez‑la « Annonces ».\n - Ajoutez un salon textuel dans cette catégorie → `#annonces`.\n - Répétez pour chaque catégorie et salon listé ci‑dessus.\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.), dites‑m‑en 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 !"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 n’apportent rien de concret. En d’autres termes, c’est 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 d’un signal sinusoïdal** :\\n ```\\n x(t) = A \\\\sin(\\\\omega t + \\\\varphi)\\n ```\\n où :\\n - **A** est l’amplitude,\\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 d’oscilloscopes** : 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 n’est 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 d’une base > 1 ne donne 0. En fait, lorsque x tend vers 0⁺, log(x) tend vers –∞, mais la valeur exacte log(0) n’existe 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 n’y a pas de signification particulière attachée au mot ; c’est généralement le nom d’un utilisateur, d’un créateur de contenu ou d’un 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 n’y a pas de signification particulière associée à ce terme ; il s’agit simplement d’une 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 l’on voit le plus souvent est **j ω** (ou parfois écrit jΩ), où :\\n- **j** est l’unité 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 d’un filtre (H) est souvent évaluée sur l’axe 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, c’est‑à‑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 (passe‑bas, passe‑haut, passe‑bande, coupe‑bande, etc.).\\n- **Analyse de stabilité** : Dans les systèmes continus, les pôles situés sur l’axe 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 passe‑bas 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- C’est l’outil principal pour analyser la réponse en fréquence et la stabilité des filtres, qu’ils 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, n’hésite pas à la partager !\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 positif x et une base b (b > 0, b ≠ 1), le logarithme base b 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 !\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 aujourd’hui ?\"}"
|
||||
},
|
||||
{
|
||||
"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\"}]}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue