624 lines
No EOL
26 KiB
Python
624 lines
No EOL
26 KiB
Python
"""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 |