2026-06-16 20:40:37 +02:00
""" Main Beta Bot class - orchestrates all components. """
2026-06-13 14:46:02 +00:00
import discord
from discord . ext import commands
import aiohttp
import asyncio
import os
import time
import json
import logging
import re
2026-06-16 20:40:37 +02:00
import datetime
import pytz
2026-06-13 14:46:02 +00:00
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
2026-06-16 20:40:37 +02:00
from . context_builder import build_context_for_message
2026-06-13 14:46:02 +00:00
from . llm import LLMManager
from . redis_worker import RedisWorker
from . commands import setup_commands
from . voice import VoiceManager
from . dispatcher import AssetDispatcher
2026-06-16 20:40:37 +02:00
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
2026-06-13 14:46:02 +00:00
2026-06-16 20:40:37 +02:00
from brain . memory import add_interaction , build_context_for_model , flush_all , MEMORY_DIR , set_llm_manager
2026-06-13 14:46:02 +00:00
from brain . moderation import init_db , log_infraction , scan_message_toxicity
from commandes . autres . config import config
from commandes . autres . logger import action_logger
2026-06-16 20:40:37 +02:00
logger = logging . getLogger ( ' Beta ' )
2026-06-13 14:46:02 +00:00
class MockMessage :
""" Mock discord.Message for voice-triggered interactions. """
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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 )
2026-06-16 20:40:37 +02:00
class Bot ( commands . Bot ) :
""" Main bot class for Beta Discord bot. """
2026-06-13 14:46:02 +00:00
def __init__ (
self ,
guild_id : int ,
llama_model ,
system_prompt : str ,
command_prefix : str ,
intents : discord . Intents ,
temperature : float = 0.4 ,
2026-06-16 20:40:37 +02:00
model_loaded : bool = True ,
favorite_ids : list = None
2026-06-13 14:46:02 +00:00
) :
super ( ) . __init__ ( command_prefix = command_prefix , intents = intents )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# Configuration
self . guild_id = guild_id
self . llama_model = llama_model
self . system_prompt = system_prompt
self . temperature = temperature
self . model_loaded = model_loaded
2026-06-16 20:40:37 +02:00
self . favorite_ids = set ( favorite_ids or [ ] )
2026-06-13 14:46:02 +00:00
# Identity
2026-06-16 20:40:37 +02:00
self . system_name = " B \u00ea ta "
2026-06-13 14:46:02 +00:00
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 ) )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# 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 ( ) ]
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# Spam Prevention
2026-06-16 20:40:37 +02:00
self . insight_cooldowns = { }
self . insight_cooldown_duration = 1800
2026-06-13 14:46:02 +00:00
self . insight_threshold = 5
2026-06-16 20:40:37 +02:00
self . ignored_words = { " salut " , " cc " , " hello " , " \u00e7 a va " , " ca va " , " bjr " , " slt " , " yo " , " re " , " ok " }
2026-06-13 14:46:02 +00:00
self . monitoring_cooldowns = { }
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# Caches
self . channels_list_cache = { }
self . server_context_cache = { }
self . _module_cache : Dict [ str , object ] = { }
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# HTTP session
self . http_session : Optional [ aiohttp . ClientSession ] = None
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# Concurrency control
self . max_concurrent_requests = int ( os . environ . get ( " SUPERVISEUR_MAX_CONCURRENT " , 4 ) )
self . _request_semaphore = asyncio . Semaphore ( self . max_concurrent_requests )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# Metrics
self . metrics = {
" total_llm_requests " : 0 ,
" total_llm_success " : 0 ,
" total_llm_errors " : 0 ,
" total_llm_time " : 0.0 ,
}
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# Redis
self . aioredis = None
try :
import redis . asyncio as aioredis
self . aioredis = aioredis
2026-06-16 20:40:37 +02:00
except ImportError :
2026-06-13 14:46:02 +00:00
pass
self . redis_worker : Optional [ RedisWorker ] = None
2026-06-16 20:40:37 +02:00
# Sub-managers
2026-06-13 14:46:02 +00:00
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 )
2026-06-16 20:40:37 +02:00
self . action_executor = ActionExecutor ( self )
2026-06-13 14:46:02 +00:00
# Init SQLite Moderation DB
init_db ( )
2026-06-16 20:40:37 +02:00
# Logging with local timezone
2026-06-13 14:46:02 +00:00
self . timezone = pytz . timezone ( os . getenv ( " TIMEZONE " , " Europe/Paris " ) )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
def local_time_converter ( * args ) :
2026-06-16 20:40:37 +02:00
return datetime . datetime . now ( self . timezone ) . timetuple ( )
2026-06-13 14:46:02 +00:00
logging . Formatter . converter = local_time_converter
logging . basicConfig (
2026-06-16 20:40:37 +02:00
level = logging . INFO ,
2026-06-13 14:46:02 +00:00
format = ' %(asctime)s - %(name)s - %(levelname)s - %(message)s '
)
2026-06-16 20:40:37 +02:00
# 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 )
2026-06-13 14:46:02 +00:00
try :
logging . getLogger ( ' discord.ext.voice_recv ' ) . setLevel ( logging . WARNING )
2026-06-16 20:40:37 +02:00
except ImportError :
pass
2026-06-13 14:46:02 +00:00
# ==================== Utility Methods ====================
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def introspection_log ( self , title : str , description : str , color : discord . Color = discord . Color . light_grey ( ) ) :
2026-06-16 20:40:37 +02:00
if not self . introspection_channel_id :
return
2026-06-16 17:09:34 +00:00
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
2026-06-13 14:46:02 +00:00
if channel :
2026-06-16 20:40:37 +02:00
await self . messaging . send_embed ( channel , f " INTROSPECTION: { title } " , description , color = color )
2026-06-13 14:46:02 +00:00
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 )
2026-06-16 20:40:37 +02:00
def has_whitelist_permission ( self , user_id : int , required_level : str , user_name : Optional [ str ] = None ) - > bool :
2026-06-13 14:46:02 +00:00
return self . whitelist_manager . has_permission ( user_id , required_level , user_name )
2026-06-16 20:40:37 +02:00
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
2026-06-13 14:46:02 +00:00
# ==================== Event Handlers ====================
2026-06-16 20:40:37 +02:00
async def on_ready ( self ) :
logger . info ( f ' Logged in as { self . user } ' )
set_llm_manager ( self . llm )
2026-06-13 14:46:02 +00:00
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 )
2026-06-16 20:40:37 +02:00
logger . debug ( f ' Memory directory created for guild { guild . name } : { guild_dir } ' )
2026-06-13 14:46:02 +00:00
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 )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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 ! " ,
2026-06-16 20:40:37 +02:00
description = f " { member . name } a quitt \u00e9 { member . guild . name } . " ,
2026-06-13 14:46:02 +00:00
color = discord . Color . red ( )
)
embed . set_thumbnail ( url = member . avatar . url if member . avatar else member . default_avatar . url )
await channel . send ( embed = embed )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_message ( self , message ) :
if message . author == self . user :
return
2026-06-16 17:09:34 +00:00
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 } ) " )
2026-06-13 14:46:02 +00:00
asyncio . create_task ( self . _handle_silent_scan ( message ) )
if isinstance ( message . channel , discord . DMChannel ) :
processed = await self . dispatcher . handle_dm_response ( message )
2026-06-16 20:40:37 +02:00
if processed :
return
2026-06-13 14:46:02 +00:00
await self . _handle_ai_interaction ( message )
return
if not self . _should_respond ( message ) :
return
2026-06-16 20:40:37 +02:00
logger . info ( f " Interaction directe de ' { message . author . display_name } ' dans # { channel_name } ( { guild_name } ) " )
2026-06-13 14:46:02 +00:00
await self . _handle_ai_interaction ( message )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# ==================== Cache Management ====================
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
def _invalidate_caches ( self , guild_id : int ) :
self . channels_list_cache . pop ( guild_id , None )
self . server_context_cache . pop ( guild_id , None )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_guild_channel_create ( self , channel ) :
self . _invalidate_caches ( channel . guild . id )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_guild_channel_delete ( self , channel ) :
self . _invalidate_caches ( channel . guild . id )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_voice_state_update ( self , member , before , after ) :
pass
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_guild_channel_update ( self , before , after ) :
self . _invalidate_caches ( before . guild . id )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_guild_role_create ( self , role ) :
self . _invalidate_caches ( role . guild . id )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_guild_role_delete ( self , role ) :
self . _invalidate_caches ( role . guild . id )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def on_guild_role_update ( self , before , after ) :
self . _invalidate_caches ( before . guild . id )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# ==================== Moderation ====================
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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 ' )
2026-06-16 20:40:37 +02:00
logger . info ( f " Infraction d \u00e9 tect \u00e9 e ( { score } /1.0) pour { message . author . display_name } : { reason } " )
2026-06-13 14:46:02 +00:00
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
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
# ==================== AI Interaction ====================
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def _handle_ai_interaction ( self , message , extra_context = " " , is_monitoring = False ) :
2026-06-16 20:40:37 +02:00
logger . debug ( f " AI interaction triggered (Monitoring: { is_monitoring } ) " )
2026-06-13 14:46:02 +00:00
if message . guild :
perms = message . author . guild_permissions
if check_is_admin ( perms ) :
self . _invalidate_caches ( message . guild . id )
2026-06-16 20:40:37 +02:00
# Role detection
2026-06-13 14:46:02 +00:00
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 "
2026-06-16 20:40:37 +02:00
author_perms = message . author . guild_permissions if message . guild else None
2026-06-13 14:46:02 +00:00
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 (
2026-06-16 20:40:37 +02:00
message . guild , self . channels_list_cache , self . server_context_cache
2026-06-13 14:46:02 +00:00
)
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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 " "
2026-06-16 20:40:37 +02:00
content = prepare_message_content ( message ) + extra_context
2026-06-13 14:46:02 +00:00
payload = self . _build_payload ( permissions_info , server_context , channels_list , history , content , message )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async with self . _request_semaphore :
async def run_call ( ) :
if self . redis_worker :
2026-06-16 20:40:37 +02:00
resp = await self . redis_worker . enqueue_job ( payload , timeout = 605 )
2026-06-13 14:46:02 +00:00
return resp . get ( " result " , " " ) if resp else " "
2026-06-16 20:40:37 +02:00
return await self . llm . call_llama ( payload )
2026-06-13 14:46:02 +00:00
try :
self . metrics [ " total_llm_requests " ] + = 1
start = time . perf_counter ( )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
if is_monitoring :
accumulated_reply = await run_call ( )
else :
async with message . channel . typing ( ) :
accumulated_reply = await run_call ( )
2026-06-16 20:40:37 +02:00
# ReAct Loop for READ_LOGS
2026-06-13 14:46:02 +00:00
if " READ_LOGS " in accumulated_reply :
2026-06-16 20:40:37 +02:00
logger . info ( " Memory access requested (READ_LOGS) " )
2026-06-13 14:46:02 +00:00
logs_context = self . get_recent_logs ( )
2026-06-16 20:40:37 +02:00
payload [ " prompt " ] + = f " \n \n [SYST \u00c8 ME: Voici les logs demand \u00e9 s.] \n { logs_context } "
2026-06-13 14:46:02 +00:00
if is_monitoring :
accumulated_reply = await run_call ( )
else :
async with message . channel . typing ( ) :
accumulated_reply = await run_call ( )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
await self . _process_response ( accumulated_reply , message , is_monitoring = is_monitoring )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
dur = time . perf_counter ( ) - start
self . metrics [ " total_llm_success " ] + = 1
self . metrics [ " total_llm_time " ] + = dur
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
except Exception as e :
try :
dur = time . perf_counter ( ) - start
self . metrics [ " total_llm_errors " ] + = 1
self . metrics [ " total_llm_time " ] + = dur
2026-06-16 20:40:37 +02:00
except ( NameError , TypeError ) :
2026-06-13 14:46:02 +00:00
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 :
2026-06-16 20:40:37 +02:00
logger . error ( f " Voice trigger FAIL - User { user_id } ( { user_name } ) introuvable. " )
2026-06-13 14:46:02 +00:00
return
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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
2026-06-16 20:40:37 +02:00
vocal_system_prompt = self . system_prompt . split ( " FORMAT DE R \u00c9 PONSE STRICT " ) [ 0 ] if " FORMAT DE R \u00c9 PONSE STRICT " in self . system_prompt else self . system_prompt
vocal_system_prompt + = (
" \n [PROTOCOLE VOCAL ACTIF] \n "
" R \u00c9 PONDEZ EXCLUSIVEMENT EN TEXTE PUR. \n "
" Soyez concis et engagez la conversation. "
2026-06-13 14:46:02 +00:00
)
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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 " "
2026-06-16 20:40:37 +02:00
mock_msg = MockMessage ( author = mock_author , channel = mock_author , content = content )
2026-06-13 14:46:02 +00:00
payload = self . llm . build_payload (
prompt = f " { permissions_info } \n Nom d ' utilisateur : { mock_author . display_name } { server_context } { channels_list } \n { history } \n \n User: { content } " ,
system_prompt = vocal_system_prompt ,
vision_model = False
)
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async with self . _request_semaphore :
2026-06-16 20:40:37 +02:00
analysis = await self . llm . call_llama ( payload )
if not analysis :
return
if " READ_LOGS " in analysis :
payload [ " prompt " ] + = f " \n \n [SYST \u00c8 ME: Voici les logs demand \u00e9 s.] \n { self . get_recent_logs ( ) } "
2026-06-13 14:46:02 +00:00
analysis = await self . llm . call_llama ( payload )
2026-06-16 20:40:37 +02:00
await self . _process_response ( analysis , mock_msg )
logger . info ( f " Voice trigger response processed for { user_name } . " )
2026-06-13 14:46:02 +00:00
except Exception as e :
2026-06-16 20:40:37 +02:00
logger . error ( f " Error in voice interaction bridge: { e } " )
2026-06-13 14:46:02 +00:00
2026-06-16 20:40:37 +02:00
# ==================== Response Processing ====================
def _build_payload ( self , permissions_info , server_context , channels_list , history , content , message ) - > dict :
2026-06-13 14:46:02 +00:00
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 \n User: { content } "
2026-06-16 20:40:37 +02:00
# 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. "
)
2026-06-13 14:46:02 +00:00
return self . llm . build_payload (
prompt = prompt ,
2026-06-16 20:40:37 +02:00
system_prompt = system_prompt ,
vision_model = False ,
2026-06-13 14:46:02 +00:00
attachments = message . attachments
)
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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
2026-06-16 20:40:37 +02:00
# 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
2026-06-13 14:46:02 +00:00
user_id = str ( message . author . id )
guild_id = sanitize_guild_name ( message . guild . name ) if message . guild else None
2026-06-16 20:40:37 +02:00
mem_response = " \n " . join ( responses ) if responses else clean_reply
if is_monitoring :
mem_response = " "
2026-06-13 14:46:02 +00:00
mem_response = re . sub ( r ' <[^>]+?> ' , ' ' , mem_response ) . strip ( )
await add_interaction ( user_id , message . channel . id , message . content , mem_response , guild_id )
2026-06-16 20:40:37 +02:00
# 5. Monitoring silence
2026-06-13 14:46:02 +00:00
if is_monitoring and not action_executed :
return
2026-06-16 20:40:37 +02:00
# 6. Send final reply
2026-06-13 14:46:02 +00:00
if not action_executed :
2026-06-16 20:40:37 +02:00
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 ? " )
2026-06-13 14:46:02 +00:00
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 )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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 "
2026-06-16 20:40:37 +02:00
f " **CONTEXTE D \u00c9 TECTION** \n "
2026-06-13 14:46:02 +00:00
f " - **Sujet** : { context_data [ ' author_mention ' ] } ( { context_data [ ' author_name ' ] } ) \n "
f " - **Salon** : { context_data [ ' channel_mention ' ] } \n "
2026-06-16 20:40:37 +02:00
+ ( f " - **Lien direct** : [Acc \u00e9 der au message]( { context_data [ ' jump_url ' ] } ) " if context_data [ ' jump_url ' ] else " - **Source** : Transmission Vocale " )
2026-06-13 14:46:02 +00:00
)
for admin_id in self . admin_ids :
admin = self . get_user ( admin_id ) or await self . fetch_user ( admin_id )
if admin :
2026-06-16 20:40:37 +02:00
await self . messaging . send_embed ( admin , f " ALERTE (Priorit \u00e9 { importance } /10) " , admin_trigger_text , color = discord . Color . red ( ) )
2026-06-13 14:46:02 +00:00
if insight and self . asset_ids :
await self . dispatcher . dispatch_insight ( insight , importance , message . guild , context_data )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def _execute_actions ( self , action_data , message , final_content , is_monitoring = False ) - > bool :
action_executed = False
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
async def process_item ( item ) :
nonlocal action_executed
if not isinstance ( item , dict ) or ' action ' not in item :
return
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
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
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
if action != ' NONE ' :
2026-06-16 20:40:37 +02:00
success , result = await self . action_executor . execute ( action , item , message )
2026-06-13 14:46:02 +00:00
action_logger . log_action ( action , str ( message . author ) , str ( message . guild ) , item , success , result )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
if not is_monitoring :
if success :
2026-06-16 20:40:37 +02:00
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 \u00e9 es ont \u00e9 t \u00e9 supprim \u00e9 es. "
}
await self . messaging . reply_with_limit ( message , f " \u25c8 { confirmations . get ( action , ' Action ex \u00e9 cut \u00e9 e. ' ) } " )
2026-06-13 14:46:02 +00:00
else :
2026-06-16 20:40:37 +02:00
await self . messaging . reply_with_limit ( message , f " \u25c8 ERREUR: { result or ' Une erreur est survenue. ' } " )
2026-06-13 14:46:02 +00:00
action_executed = True
if isinstance ( action_data , list ) :
executed = set ( )
for item in action_data :
key = json . dumps ( item , sort_keys = True )
2026-06-16 20:40:37 +02:00
if key in executed :
continue
2026-06-13 14:46:02 +00:00
executed . add ( key )
await process_item ( item )
elif isinstance ( action_data , dict ) :
await process_item ( action_data )
2026-06-16 20:40:37 +02:00
2026-06-13 14:46:02 +00:00
return action_executed
2026-06-16 20:40:37 +02:00
# ==================== Shutdown ====================
2026-06-13 14:46:02 +00:00
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 ( )
2026-06-16 20:40:37 +02:00
# Backward compatibility alias
Superviseur = Bot