fix: gpt-oss:20b ollama, streaming, tableaux JSON, recherche flexible salons/categories
This commit is contained in:
parent
14985f6dbb
commit
189d56026b
21 changed files with 2824 additions and 491 deletions
|
|
@ -21,6 +21,9 @@ from .commands import setup_commands
|
|||
from .voice import VoiceManager
|
||||
from .voice import VoiceManager
|
||||
from .dispatcher import AssetDispatcher
|
||||
from .tasks import TaskManager
|
||||
from .action_router import ActionRouter
|
||||
from .heuristic import HeuristicsManager
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
|
|
@ -58,29 +61,21 @@ class Superviseur(commands.Bot):
|
|||
def __init__(
|
||||
self,
|
||||
guild_id: int,
|
||||
ollama_api_url: str,
|
||||
ollama_model: str,
|
||||
ollama_timeout: int,
|
||||
llama_model,
|
||||
system_prompt: str,
|
||||
command_prefix: str,
|
||||
intents: discord.Intents,
|
||||
ollama_temperature: float = 0.7,
|
||||
ollama_api_key: Optional[str] = None,
|
||||
ollama_moderation_model: Optional[str] = None,
|
||||
ollama_model_fast: Optional[str] = None
|
||||
temperature: float = 0.4,
|
||||
model_loaded: bool = True
|
||||
):
|
||||
super().__init__(command_prefix=command_prefix, intents=intents)
|
||||
|
||||
# Configuration
|
||||
self.guild_id = guild_id
|
||||
self.ollama_api_url = ollama_api_url
|
||||
self.ollama_model = ollama_model
|
||||
self.ollama_model_fast = ollama_model_fast or ollama_model
|
||||
self.ollama_moderation_model = ollama_moderation_model or ollama_model
|
||||
self.ollama_timeout = ollama_timeout
|
||||
self.llama_model = llama_model
|
||||
self.system_prompt = system_prompt
|
||||
self.ollama_temperature = ollama_temperature
|
||||
self.ollama_api_key = ollama_api_key
|
||||
self.temperature = temperature
|
||||
self.model_loaded = model_loaded
|
||||
|
||||
# Identity
|
||||
self.system_name = "Bêta"
|
||||
|
|
@ -142,14 +137,15 @@ class Superviseur(commands.Bot):
|
|||
|
||||
# LLM
|
||||
self.llm = LLMManager(
|
||||
ollama_api_url=ollama_api_url,
|
||||
ollama_model=ollama_model,
|
||||
ollama_timeout=ollama_timeout,
|
||||
ollama_temperature=ollama_temperature
|
||||
llama_model=llama_model,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
# Dispatcher
|
||||
# Dispatcher & Sub-managers
|
||||
self.dispatcher = AssetDispatcher(self)
|
||||
self.task_manager = TaskManager(self)
|
||||
self.action_router = ActionRouter(self)
|
||||
self.heuristics_manager = HeuristicsManager(self)
|
||||
|
||||
# Logging avec fuseau horaire local (Europe/Paris)
|
||||
self.timezone = pytz.timezone(os.getenv("TIMEZONE", "Europe/Paris"))
|
||||
|
|
@ -217,156 +213,14 @@ class Superviseur(commands.Bot):
|
|||
os.makedirs(guild_dir, exist_ok=True)
|
||||
print(f'Memory directory created for guild {guild.name}: {guild_dir}')
|
||||
|
||||
# Setup HTTP session
|
||||
await self.llm.get_session()
|
||||
# Setup HTTP session (not needed for llama-cpp-python, but kept for compatibility)
|
||||
pass
|
||||
|
||||
# Setup Redis
|
||||
# Start background tasks
|
||||
await self.voice_manager.initialize()
|
||||
self.loop.create_task(self._introspection_report_loop())
|
||||
self.loop.create_task(self._voice_report_loop())
|
||||
self.loop.create_task(self._voice_report_loop())
|
||||
self.loop.create_task(self._daily_report_check_loop())
|
||||
self.loop.create_task(self._gdpr_purge_loop())
|
||||
|
||||
# Start interviews for missing assets
|
||||
for asset_id in self.asset_ids:
|
||||
if str(asset_id) not in self.dispatcher.assets:
|
||||
self.loop.create_task(self.dispatcher.start_interview(asset_id))
|
||||
self.task_manager.start_all()
|
||||
|
||||
async def _voice_evaluation_loop(self):
|
||||
"""Periodic check for tactical voice connections."""
|
||||
await self.wait_until_ready()
|
||||
while not self.is_closed():
|
||||
for guild in self.guilds:
|
||||
await self.voice_manager.evaluate_and_connect(guild)
|
||||
await asyncio.sleep(30) # Reduit à 30s pour plus de réactivité en tâche de fond
|
||||
|
||||
async def _introspection_report_loop(self):
|
||||
"""Periodic broadcast of system health metrics."""
|
||||
await self.wait_until_ready()
|
||||
while not self.is_closed():
|
||||
await asyncio.sleep(600) # Every 10 minutes
|
||||
await self._send_health_report()
|
||||
|
||||
async def _send_health_report(self):
|
||||
"""Build and send a clinical health report embed."""
|
||||
# Calculate success rate
|
||||
total = self.metrics["total_llm_requests"]
|
||||
success = self.metrics["total_llm_success"]
|
||||
rate = (success / total * 100) if total > 0 else 100
|
||||
avg_time = (self.metrics["total_llm_time"] / success) if success > 0 else 0
|
||||
|
||||
status_report = (
|
||||
f"**◈ INTELLIGENCE CORE**\n"
|
||||
f"- Requests Processed : `{total}`\n"
|
||||
f"- Reliability Rate : `{rate:.1f}%`\n"
|
||||
f"- Latency Heuristic : `{avg_time:.2f}s` (Target: < 5s)\n\n"
|
||||
f"**◈ TACTICAL OVERVIEW**\n"
|
||||
f"- Voice Protocol : `READY`\n"
|
||||
f"- Active Deployment : `{self.voice_manager.current_channel.name if self.voice_manager.current_channel else 'NONE'}`\n"
|
||||
)
|
||||
|
||||
await self.introspection_log(
|
||||
"SYSTEM STATUS REPORT",
|
||||
status_report,
|
||||
discord.Color.dark_blue()
|
||||
)
|
||||
|
||||
async def _daily_report_check_loop(self):
|
||||
"""Check for 21:30 and trigger the daily report using local timezone."""
|
||||
await self.wait_until_ready()
|
||||
report_sent_today = False
|
||||
while not self.is_closed():
|
||||
now = datetime.datetime.now(self.timezone)
|
||||
if now.hour == 21 and now.minute == 30:
|
||||
if not report_sent_today:
|
||||
await self._send_daily_summary()
|
||||
report_sent_today = True
|
||||
else:
|
||||
report_sent_today = False
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def _gdpr_purge_loop(self):
|
||||
"""Daily purge of old memory files (30 days) for GDPR compliance."""
|
||||
from ia.memoire import purge_old_memories
|
||||
while not self.is_closed():
|
||||
# Run purge at 03:00 AM
|
||||
now = datetime.datetime.now(self.timezone)
|
||||
if now.hour == 3 and now.minute == 0:
|
||||
logger.info("◈ RGPD: Démarrage de la purge quotidienne...")
|
||||
deleted = await purge_old_memories(days=30)
|
||||
logger.info(f"◈ RGPD: Purge terminée. {deleted} fichiers supprimés.")
|
||||
await asyncio.sleep(65) # Avoid multiple triggers in the same minute
|
||||
await asyncio.sleep(30)
|
||||
|
||||
async def _send_daily_summary(self):
|
||||
"""Generate and send a dense, highly detailed clinical report at 21:30."""
|
||||
tasks = self.dispatcher.tasks
|
||||
tense_sessions = self.metrics.get("daily_tense_sessions", 0)
|
||||
|
||||
# 1. Statistiques par Asset
|
||||
asset_stats = {} # {asset_id: {"name": str, "accepted": 0, "missed": 0}}
|
||||
for t_id, t_data in tasks.items():
|
||||
a_id = t_data.get("asset_id")
|
||||
if a_id not in asset_stats:
|
||||
asset_name = "Inconnu"
|
||||
if str(a_id) in self.dispatcher.assets:
|
||||
asset_name = self.dispatcher.assets[str(a_id)].get("name", "Anonyme")
|
||||
asset_stats[a_id] = {"name": asset_name, "accepted": 0, "missed": 0, "total": 0}
|
||||
|
||||
asset_stats[a_id]["total"] += 1
|
||||
if t_data["status"] == "ACCEPTED":
|
||||
asset_stats[a_id]["accepted"] += 1
|
||||
elif t_data["status"] == "PENDING" and t_data.get("deadline", 0) < time.time():
|
||||
asset_stats[a_id]["missed"] += 1
|
||||
|
||||
stats_lines = []
|
||||
for a_id, s in asset_stats.items():
|
||||
stats_lines.append(f"- **{s['name']}** : `{s['accepted']}/{s['total']}` acceptées, `{s['missed']}` manquées.")
|
||||
|
||||
# 2. Liste détaillée des tâches
|
||||
task_details = []
|
||||
for t_id, t_data in list(tasks.items())[-15:]: # On prend les 15 dernières
|
||||
status_emoji = "✅" if t_data["status"] == "ACCEPTED" else "❌" if t_data.get("deadline", 0) < time.time() else "⏳"
|
||||
asset_name = self.dispatcher.assets.get(str(t_data['asset_id']), {}).get('name', 'N/A')
|
||||
task_details.append(f"{status_emoji} `{t_id}` | Staff: {asset_name} | Urgence: {t_data['importance']} | Context: {t_data['content'][:50]}...")
|
||||
|
||||
summary = (
|
||||
f"**◈ RAPPORT ANALYTIQUE DE FIN DE CYCLE ◈**\n"
|
||||
f"Période : Dernières 24h | Heure : 21h30\n\n"
|
||||
f"**◈ BILAN GLOBAL DES OPÉRATIONS**\n"
|
||||
f"- Volume total d'actions de modération : `{len(tasks)}` \n"
|
||||
f"- Sessions vocales sous tension : `{tense_sessions}`\n\n"
|
||||
f"**◈ PERFORMANCE DES ASSETS**\n"
|
||||
+ ("\n".join(stats_lines) if stats_lines else "Aucune activité de staff enregistrée.") + "\n\n"
|
||||
f"**◈ LOG DÉTAILLÉ DES DERNIERS SIGNAUX**\n"
|
||||
+ ("\n".join(task_details) if task_details else "Aucun signal capturé.") + "\n\n"
|
||||
f"**◈ CONCLUSION HEURISTIQUE**\n"
|
||||
f"Le système est nominal. " + ("PRÉCONISATION : Vigilance accrue requise sur les membres suspects." if tense_sessions > 0 else "État de l'écosystème : Stable.")
|
||||
)
|
||||
|
||||
# 4. Envoi à tous les administrateurs
|
||||
if summary and self.admin_ids:
|
||||
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, "SYNTHÈSE CLINIQUE QUOTIDIENNE", summary, color=discord.Color.dark_purple())
|
||||
|
||||
# Reset pour le lendemain
|
||||
self.metrics["daily_tense_sessions"] = 0
|
||||
# On pourrait purger tasks ici mais on va les garder pour l'instant (ou purger les vieilles)
|
||||
self.dispatcher.tasks = {k: v for k, v in tasks.items() if v['status'] == 'PENDING' and v.get('deadline', 0) > time.time()}
|
||||
self.dispatcher._save_data(self.dispatcher.tasks, os.path.join(os.path.dirname(__file__), "..", "active_tasks.json"))
|
||||
|
||||
async def _voice_report_loop(self):
|
||||
"""Periodic broadcast of active voice session summaries."""
|
||||
await self.wait_until_ready()
|
||||
while not self.is_closed():
|
||||
await asyncio.sleep(900) # Every 15 minutes
|
||||
if self.voice_manager.is_monitoring and self.voice_manager.session_buffer:
|
||||
report = await self.voice_manager.generate_session_report()
|
||||
await self.introspection_log("VOICE SESSION UPDATE (15m)", report, discord.Color.blue())
|
||||
|
||||
async def on_member_join(self, member):
|
||||
"""Handle member join."""
|
||||
|
|
@ -471,104 +325,6 @@ class Superviseur(commands.Bot):
|
|||
|
||||
# ==================== Heuristic Engine (Risk Assessment) ====================
|
||||
|
||||
def _assess_heuristic_risk(self, message) -> bool:
|
||||
"""
|
||||
Fast Python-side risk assessment to decide if we should AI-analyze fully.
|
||||
Returns True if message is 'risky' enough to warrant LLM usage.
|
||||
"""
|
||||
score = 0
|
||||
content = message.content
|
||||
|
||||
# CLEAN content for heuristic analysis (ignore mentions in the score)
|
||||
# We use a copy to avoid modifying the original message object
|
||||
scoring_content = re.sub(r'<@!?\d+>', '', content).strip()
|
||||
if not scoring_content: scoring_content = content
|
||||
|
||||
# 1. Content Scoring
|
||||
score = 0
|
||||
|
||||
# Proactive Analysis: base score if message has substance
|
||||
if len(content) > 10:
|
||||
score += 15
|
||||
|
||||
scoring_content = content
|
||||
scoring_content_lower = scoring_content.lower()
|
||||
|
||||
# CAPS LOCK detection (> 35% caps on messages > 4 chars)
|
||||
if len(scoring_content) > 4:
|
||||
caps_ratio = sum(1 for c in scoring_content if c.isupper()) / len(scoring_content)
|
||||
if caps_ratio > 0.35:
|
||||
score += 35 # Heavy weight
|
||||
logger.debug(f"◈ HEURISTIC: CAPS detected ({caps_ratio:.2f}) on '{scoring_content[:15]}...'")
|
||||
|
||||
# Profanity / Trigger words (lightweight list)
|
||||
triggers = [
|
||||
"raid", "hack", "pute", "connard", "fdp", "tg", "merde", "nazi", "hitler",
|
||||
"suicide", "bomb", "token", "grab", "nitro", "steam", "discord.gift", "free",
|
||||
"@everyone", "@here", "detruire", "detruit", "destruction", "kill"
|
||||
]
|
||||
for t in triggers:
|
||||
if t in scoring_content_lower:
|
||||
score += 50
|
||||
logger.debug(f"◈ HEURISTIC: Trigger word '{t}' detected")
|
||||
break
|
||||
|
||||
# Link spam detection
|
||||
if "http" in scoring_content_lower:
|
||||
score += 20
|
||||
|
||||
# Length anomaly
|
||||
if len(scoring_content) > 300:
|
||||
score += 20
|
||||
|
||||
# 3. Gibberish / Randomness detection (Entropy)
|
||||
if len(scoring_content) > 8:
|
||||
unique_chars = len(set(scoring_content_lower))
|
||||
diversity_ratio = unique_chars / len(scoring_content)
|
||||
if diversity_ratio < 0.35: # Increased threshold
|
||||
score += 25
|
||||
logger.debug(f"◈ HEURISTIC: Low diversity detected ({diversity_ratio:.2f})")
|
||||
|
||||
# Too many non-alphanumeric chars (excluding spaces)
|
||||
symbols = len(re.findall(r'[^a-zA-Z0-9\s]', scoring_content))
|
||||
symbol_ratio = symbols / len(scoring_content)
|
||||
if symbol_ratio > 0.15: # Lowered threshold for symbols
|
||||
score += 30
|
||||
logger.debug(f"◈ HEURISTIC: High symbol ratio detected ({symbol_ratio:.2f})")
|
||||
|
||||
# 4. Leetspeak / Digits in words detection
|
||||
if len(re.findall(r'[a-zA-Z][0-9][a-zA-Z]', scoring_content)) > 0:
|
||||
score += 25
|
||||
logger.debug("◈ HEURISTIC: Leetspeak patterns detected")
|
||||
|
||||
# 5. Long word detection (No spaces)
|
||||
words = scoring_content.split()
|
||||
if any(len(w) > 25 for w in words):
|
||||
score += 30
|
||||
logger.debug("◈ HEURISTIC: Unusually long word detected")
|
||||
|
||||
# 3. Mention Spam
|
||||
# Fallback detection if library fails to populate message.mentions
|
||||
mention_count = len(message.mentions)
|
||||
if mention_count == 0:
|
||||
mention_count = len(re.findall(r'<@!?\d+>', content))
|
||||
|
||||
if mention_count > 3:
|
||||
score += 40
|
||||
logger.debug(f"◈ HEURISTIC: Mention spam detected ({mention_count} mentions)")
|
||||
|
||||
# 4. Keyword Detection (Sensitivity helper)
|
||||
if score < 45:
|
||||
for trigger in ["analyse", "écouter", "surveille", "check"]:
|
||||
if trigger in scoring_content_lower:
|
||||
score += 30
|
||||
break
|
||||
|
||||
# Threshold decision
|
||||
# Very sensitive threshold to capture more nuances (as requested)
|
||||
logger.info(f"◈ MODERATION RISK: Score Final = {score}/15")
|
||||
return score >= 15
|
||||
|
||||
# ==================== Moderation ====================
|
||||
|
||||
async def _handle_moderation(self, message):
|
||||
|
|
@ -582,7 +338,7 @@ class Superviseur(commands.Bot):
|
|||
if len(content_low) < 3 or content_low in self.ignored_words:
|
||||
return
|
||||
|
||||
should_analyze = self._assess_heuristic_risk(message)
|
||||
should_analyze = self.heuristics_manager.assess_risk(message)
|
||||
if not should_analyze:
|
||||
return
|
||||
|
||||
|
|
@ -627,11 +383,6 @@ class Superviseur(commands.Bot):
|
|||
logger.info("◈ DEBUG: Triggered by string match (Regex Fallback)")
|
||||
return True
|
||||
|
||||
# Détection du nom "Bêta" ou "Beta" (insensible à la casse)
|
||||
content_low = message.content.lower()
|
||||
if "bêta" in content_low or "beta" in content_low:
|
||||
logger.info("◈ DEBUG: Triggered by name match (Bêta/Beta)")
|
||||
return True
|
||||
|
||||
if message.reference and message.reference.resolved:
|
||||
if message.reference.resolved.author == self.user:
|
||||
|
|
@ -690,15 +441,15 @@ class Superviseur(commands.Bot):
|
|||
# Modèle HEAVY pour Admin/Asset ou si on lui parle directement
|
||||
# Modèle FAST pour le monitoring pur (unloads the server)
|
||||
is_direct = self._should_respond(message) or message.author.id in self.admin_ids
|
||||
target_model = self.ollama_model if is_direct else self.ollama_model_fast
|
||||
payload["model"] = target_model
|
||||
# For llama-cpp-python, we don't need to specify different models in payload
|
||||
# The model is already set in the LLMManager initialization
|
||||
|
||||
async def run_call():
|
||||
if self.redis_worker:
|
||||
resp = await self.redis_worker.enqueue_job(payload, timeout=self.ollama_timeout + 5)
|
||||
resp = await self.redis_worker.enqueue_job(payload, timeout=605) # 10 minutes + 5 seconds
|
||||
return resp.get("result", "") if resp else ""
|
||||
else:
|
||||
return await self.llm.call_ollama(payload)
|
||||
return await self.llm.call_llama(payload)
|
||||
|
||||
try:
|
||||
self.metrics["total_llm_requests"] += 1
|
||||
|
|
@ -803,11 +554,10 @@ class Superviseur(commands.Bot):
|
|||
system_prompt=vocal_system_prompt,
|
||||
vision_model=False
|
||||
)
|
||||
payload["model"] = self.ollama_model
|
||||
|
||||
async with self._request_semaphore:
|
||||
try:
|
||||
analysis = await self.llm.call_ollama(payload)
|
||||
analysis = await self.llm.call_llama(payload)
|
||||
if not analysis:
|
||||
logger.warning(f"◈ SYSTEM: LLM returned empty analysis for voice trigger.")
|
||||
return
|
||||
|
|
@ -820,7 +570,7 @@ class Superviseur(commands.Bot):
|
|||
extra_memory_context = f"\n\n[SYSTÈME: Voici les logs demandés. Utilisez-les pour répondre.]\n{logs_context}"
|
||||
payload["prompt"] += extra_memory_context
|
||||
|
||||
analysis = await self.llm.call_ollama(payload)
|
||||
analysis = await self.llm.call_llama(payload)
|
||||
# ----------------------------------------------------
|
||||
|
||||
logger.debug(f"◈ DEBUG: Raw vocal response: {analysis[:100]}...")
|
||||
|
|
@ -868,7 +618,7 @@ class Superviseur(commands.Bot):
|
|||
|
||||
def get_recent_logs(self, limit: int = 15) -> str:
|
||||
"""Read and format recent action logs for context."""
|
||||
log_path = "actions.log"
|
||||
log_path = "data/actions.log"
|
||||
if not os.path.exists(log_path):
|
||||
return ""
|
||||
|
||||
|
|
@ -941,10 +691,12 @@ class Superviseur(commands.Bot):
|
|||
) -> dict:
|
||||
"""Build LLM payload."""
|
||||
user_name_info = f"Nom d'utilisateur : {message.author.display_name} (ID: {message.author.id})"
|
||||
prompt = f"{self.system_prompt}\n{permissions_info}\n{user_name_info}{server_context}{channels_list}\n{history}\n\nUser: {content}"
|
||||
# Removed redundant self.system_prompt as it's passed as system_prompt in build_payload
|
||||
prompt = f"{permissions_info}\n{user_name_info}{server_context}{channels_list}\n{history}\n\nUser: {content}"
|
||||
|
||||
vision_model = any(v in self.ollama_model.lower() for v in
|
||||
['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']) and message.attachments
|
||||
# For llama-cpp-python, we don't need to check for vision models in the same way
|
||||
# Vision support would need to be handled differently with llama-cpp-python
|
||||
vision_model = False # Simplified for now
|
||||
|
||||
return self.llm.build_payload(
|
||||
prompt=prompt,
|
||||
|
|
@ -963,7 +715,7 @@ class Superviseur(commands.Bot):
|
|||
|
||||
# 1. Enlever tout ce qui ressemble à du JSON de la réponse "publique"
|
||||
if json_str:
|
||||
clean_reply = re.sub(r'\{[\s\n]*[\"\']action[\"\'].*?\}', '', clean_reply, flags=re.DOTALL).strip()
|
||||
clean_reply = re.sub(r'\{[\s\n]*[\"\']action[\"\'].*?\}', '', clean_reply, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||
|
||||
# 2. Nettoyage des éventuels backticks de code et résidus de JSON
|
||||
clean_reply = clean_reply.replace('```json', '').replace('```', '').strip()
|
||||
|
|
@ -987,18 +739,22 @@ class Superviseur(commands.Bot):
|
|||
clean_reply = clean_reply.replace(marker, "")
|
||||
|
||||
# 4. Supprimer les résidus JSON rémanents avec un regex plus large
|
||||
clean_reply = re.sub(r'\{[\s\n]*[\"\']action[\"\'].*?\}', '', clean_reply, flags=re.DOTALL)
|
||||
clean_reply = re.sub(r'\{[\s\n]*[\"\']action[\"\'].*?\}', '', clean_reply, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# 5. Supprimer les lignes vides en trop et les espaces multiples
|
||||
clean_reply = re.sub(r'\n{3,}', '\n\n', clean_reply)
|
||||
clean_reply = clean_reply.strip()
|
||||
|
||||
is_explicit_none = False
|
||||
if json_str:
|
||||
try:
|
||||
actions = json.loads(json_str)
|
||||
if isinstance(actions, dict): actions = [actions]
|
||||
|
||||
for action_data in actions:
|
||||
if action_data.get('action', '').upper() == 'NONE' and not action_data.get('response'):
|
||||
is_explicit_none = True
|
||||
|
||||
# Traitement Alerte/Insight (Toujours en DM)
|
||||
await self._handle_beta_signals(action_data, message)
|
||||
|
||||
|
|
@ -1022,7 +778,7 @@ class Superviseur(commands.Bot):
|
|||
if not action_executed:
|
||||
if clean_reply:
|
||||
await self.messaging.reply_with_limit(message, format_mentions_in_text(clean_reply))
|
||||
elif not is_monitoring:
|
||||
elif not is_monitoring and not is_explicit_none:
|
||||
# Log the raw response to understand why parsing failed
|
||||
raw_snippet = accumulated_reply.strip()
|
||||
logger.warning(f"◈ SYSTEM: Failed to parse action or empty response. Raw output: {raw_snippet[:200]}")
|
||||
|
|
@ -1087,13 +843,14 @@ class Superviseur(commands.Bot):
|
|||
return
|
||||
|
||||
action = item['action']
|
||||
if action == 'NONE':
|
||||
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
|
||||
else:
|
||||
success, result = await self.execute_action(action, item, message)
|
||||
|
||||
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_router.execute_action(action, item, message)
|
||||
action_logger.log_action(action, str(message.author), str(message.guild), item, success, result)
|
||||
|
||||
# Feedback to user for systemic actions if not in monitoring
|
||||
|
|
@ -1129,149 +886,6 @@ class Superviseur(commands.Bot):
|
|||
await process_item(action_data)
|
||||
|
||||
return action_executed
|
||||
|
||||
# ==================== Action Execution ====================
|
||||
|
||||
async def execute_action(self, action: str, params: Dict, message) -> (bool, str):
|
||||
"""Execute an action from LLM response."""
|
||||
|
||||
# --- INSIGHT HOOK (LLM generated task for Staff) ---
|
||||
if action == 'INSIGHT':
|
||||
# Extract content from params
|
||||
content = params.get('message') or params.get('content') or params.get('insight') or "Pas de contenu"
|
||||
level = params.get('level', 'STAFF')
|
||||
|
||||
# Update Social Score if relevant
|
||||
if 'conflit' in content.lower() or 'tension' in content.lower():
|
||||
logger.debug("Insight (Tension) logged.")
|
||||
|
||||
# Dispatch
|
||||
# We reconstruct a minimal context if not present
|
||||
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": message.content[:200]
|
||||
}
|
||||
|
||||
await self.dispatcher.dispatch_insight(
|
||||
content=content,
|
||||
importance=params.get('importance', 2),
|
||||
guild=message.guild,
|
||||
context=context_data
|
||||
)
|
||||
return True, None
|
||||
# -----------------------------
|
||||
|
||||
# --- ALERT HOOK (LLM generated alert for Admin) ---
|
||||
if action == 'ALERT':
|
||||
content = params.get('message') or params.get('content') or params.get('alert') or "Alerte vide"
|
||||
|
||||
# Send to Admins
|
||||
embed = discord.Embed(title="🚨 ALERTE DIRECTE (IA)", description=content, color=discord.Color.red())
|
||||
embed.set_footer(text=f"Source: {message.author.display_name} | Salon: {message.channel}")
|
||||
|
||||
for admin_id in self.admin_ids:
|
||||
u = self.get_user(admin_id)
|
||||
if u: await u.send(embed=embed)
|
||||
|
||||
return True, None
|
||||
# -----------------------------
|
||||
|
||||
# --- VOICE ACTIONS ---
|
||||
if action == 'JOIN_VOICE':
|
||||
# Resilience: check message author, but also attempt to find them in the guild
|
||||
# if we are in a DM interaction or if cache is stale.
|
||||
voice_state = getattr(message.author, 'voice', None)
|
||||
|
||||
if not voice_state and self.guild_id:
|
||||
guild = self.get_guild(self.guild_id)
|
||||
if guild:
|
||||
member = guild.get_member(message.author.id) or await guild.fetch_member(message.author.id)
|
||||
if member:
|
||||
voice_state = member.voice
|
||||
|
||||
if voice_state and voice_state.channel:
|
||||
await self.voice_manager.join_channel(voice_state.channel)
|
||||
return True, f"Connecté à {voice_state.channel.name}"
|
||||
else:
|
||||
return False, "Vous n'êtes pas dans un salon vocal identifiable par le système."
|
||||
|
||||
if action == 'LEAVE_VOICE':
|
||||
await self.voice_manager.leave_current()
|
||||
return True, None
|
||||
|
||||
# --- PRIVACY ACTIONS ---
|
||||
if action == 'FORGET_USER':
|
||||
from ia.memoire import delete_user_memory
|
||||
delete_user_memory(message.author.id)
|
||||
return True, None
|
||||
|
||||
# Check whitelist
|
||||
if self.whitelist_manager.whitelist:
|
||||
required_level = self.whitelist_manager.get_required_level(action)
|
||||
if required_level and not self.has_whitelist_permission(message.author.id, required_level):
|
||||
return False, f"Niveau whitelist requis: {required_level}"
|
||||
|
||||
action_map = {
|
||||
'KICK': 'commandes.security.kick',
|
||||
'BAN': 'commandes.security.ban',
|
||||
'UNBAN': 'commandes.security.unban',
|
||||
'UNMUTE': 'commandes.security.unmute',
|
||||
'MUTE': 'commandes.security.mute',
|
||||
'TIMEOUT': 'commandes.security.timeout',
|
||||
'PURGE': 'commandes.security.purge',
|
||||
'WARN': 'commandes.security.warn',
|
||||
'LIST_WARNINGS': 'commandes.security.list_warnings',
|
||||
'CREATE_CHANNEL': 'commandes.salons.creer',
|
||||
'DELETE_CHANNEL': 'commandes.salons.supprimer',
|
||||
'MODIFY_CHANNEL': 'commandes.salons.modifier',
|
||||
'RENAME_CHANNEL': 'commandes.salons.renommer',
|
||||
'MOVE_CHANNEL': 'commandes.salons.deplacer',
|
||||
'CREATE_ROLE': 'commandes.roles.creer',
|
||||
'DELETE_ROLE': 'commandes.roles.supprimer',
|
||||
'MODIFY_ROLE': 'commandes.roles.modifier',
|
||||
'RENAME_ROLE': 'commandes.roles.renommer',
|
||||
'MOVE_ROLE': 'commandes.roles.deplacer',
|
||||
'ADD_ROLE_TO_USER': 'commandes.roles.add_role',
|
||||
'REMOVE_ROLE_FROM_USER': 'commandes.roles.remove_role',
|
||||
'CREATE_CATEGORY': 'commandes.categories.creer',
|
||||
'DELETE_CATEGORY': 'commandes.categories.supprimer',
|
||||
'MODIFY_CATEGORY': 'commandes.categories.modifier',
|
||||
'RENAME_CATEGORY': 'commandes.categories.renommer',
|
||||
'MOVE_CATEGORY': 'commandes.categories.deplacer',
|
||||
'PING': 'commandes.autres.ping',
|
||||
'SET_WELCOME_CHANNEL': 'commandes.autres.setwelcomechannel',
|
||||
'SET_GOODBYE_CHANNEL': 'commandes.autres.setgoodbyechannel',
|
||||
'SET_MUTE_ROLE': 'commandes.autres.setmuterole',
|
||||
'SEND_MESSAGE': 'commandes.autres.send_message',
|
||||
'HELP': 'commandes.autres.help',
|
||||
'STATUS': 'commandes.autres.status',
|
||||
}
|
||||
|
||||
module_name = action_map.get(action)
|
||||
if module_name:
|
||||
logger.debug(f"Importing module: {module_name}")
|
||||
try:
|
||||
module = self._module_cache.get(module_name)
|
||||
if module is None:
|
||||
module = __import__(module_name, fromlist=['execute'])
|
||||
self._module_cache[module_name] = module
|
||||
|
||||
logger.debug(f"Module {module_name} imported successfully")
|
||||
await module.execute(self, params, message)
|
||||
logger.info(f"Action '{action}' executed successfully")
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Error during action execution '{action}': {error_msg}")
|
||||
return False, error_msg
|
||||
else:
|
||||
error_msg = f"Unknown action: {action}"
|
||||
logger.error(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
# ==================== Metrics & Cleanup ====================
|
||||
|
||||
def get_metrics(self) -> Dict:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue