chore: nettoyage .gitignore - retrait venv, __pycache__, .env, data, logs, modèles du tracking
This commit is contained in:
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.
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
|
||||
|
|
@ -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) ---
|
||||
|
|
|
|||
770
core/bot.py
770
core/bot.py
File diff suppressed because it is too large
Load diff
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()
|
||||
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
|
||||
102
core/llm.py
102
core/llm.py
|
|
@ -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
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
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue