chore: nettoyage .gitignore - retrait venv, __pycache__, .env, data, logs, modèles du tracking

This commit is contained in:
Lowei 2026-06-16 20:40:37 +02:00
parent 7333a22bcd
commit 604a5affe0
9231 changed files with 1244 additions and 1601201 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

7
core/action_executor.py Normal file
View file

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

View file

@ -2,16 +2,16 @@ import discord
import logging
from typing import Dict
logger = logging.getLogger('Superviseur')
logger = logging.getLogger('Beta')
class ActionRouter:
"""Routs generated AI semantic actions to the physical Discord moderative or administrative commands."""
class ActionExecutor:
"""Routes generated AI semantic actions to Discord administrative commands."""
def __init__(self, bot):
self.bot = bot
self._module_cache = {}
async def execute_action(self, action: str, params: Dict, message: discord.Message) -> (bool, str):
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
@ -82,8 +82,12 @@ class ActionRouter:
# --- PRIVACY ACTIONS ---
if action == 'FORGET_USER':
from brain.memoire import delete_user_memory
delete_user_memory(message.author.id)
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) ---

File diff suppressed because it is too large Load diff

37
core/content.py Normal file
View file

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

4
core/context_builder.py Normal file
View file

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

View file

@ -22,7 +22,6 @@ class LLMManager:
self.model_name = model_name
self.base_url = base_url or os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
self.temperature = temperature
self._lock = asyncio.Lock()
def build_payload(
self,
@ -51,60 +50,59 @@ class LLMManager:
async def call_llama(self, payload: Dict[str, Any]) -> str:
"""Call Ollama Generate API and stream response."""
async with self._lock:
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", [])
}
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"
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()
# 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 ""
except Exception as e:
logger.error(f"Error calling Ollama: {e}")
return ""
async def close_session(self):
pass

83
core/log_parser.py Normal file
View file

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

75
core/response_handler.py Normal file
View file

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

4
core/task_manager.py Normal file
View file

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

View file

@ -5,7 +5,7 @@ import time
import os
import discord
logger = logging.getLogger('Superviseur')
logger = logging.getLogger('Beta')
class TaskManager:
"""Handles global background tasks, scheduled loops and reporting."""
@ -29,10 +29,17 @@ class TaskManager:
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:
await self.bot.voice_manager.evaluate_and_connect(guild)
await asyncio.sleep(30) # Reduit à 30s pour plus de réactivité en tâche de fond
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."""

View file

@ -6,7 +6,7 @@ import time
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:
@ -116,12 +116,7 @@ class VoiceManager:
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
@ -150,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
@ -164,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."""