Beta/beta.py

215 lines
8.9 KiB
Python

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)