import json import os import time import datetime import logging from typing import Dict, List, Optional import discord from .permissions import check_is_admin logger = logging.getLogger('Superviseur') ASSETS_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "assets_config.json") TASKS_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "active_tasks.json") class AssetDispatcher: """Manages autonomous staff coordination and task assignments.""" def __init__(self, bot): self.bot = bot self.assets = self._load_data(ASSETS_DATA_PATH) self.tasks = self._load_data(TASKS_DATA_PATH) self.interview_states = {} # {user_id: bool} def _load_data(self, path): if os.path.exists(path): with open(path, 'r') as f: return json.load(f) return {} def _save_data(self, data, path): with open(path, 'w') as f: json.dump(data, f, indent=2) async def start_interview(self, user_id: int): """Initiate the autonomous interview protocol via DM.""" if str(user_id) in self.assets: return user = self.bot.get_user(user_id) or await self.bot.fetch_user(user_id) if not user: return self.interview_states[user_id] = True prompt = ( "◈ PROTOCOLE D'ENTRETIEN : ACQUISITION DE DISPONIBILITÉS ◈\n\n" "Bonjour Asset Primaire. Pour optimiser la coordination du système, j'ai besoin de connaître vos disponibilités.\n" "Veuillez m'indiquer vos horaires habituels (cours, travail, repos) ainsi que votre fuseau horaire.\n" "Répondez-moi naturellement, je traiterai vos données." ) await self.bot.messaging.send_embed(user, "COMMUNICATION ENTRANTE", prompt, color=discord.Color.blue(), is_asset=True) async def handle_dm_response(self, message: discord.Message): """Process natural language response from an Asset for their schedule.""" u_id = str(message.author.id) if u_id not in self.interview_states: return False # Indiquer que Bêta réfléchit async with message.channel.typing(): # Use LLM to extract schedule prompt = ( f"ANALYSE DE DISPONIBILITÉ ASSET\n" f"Texte du membre : \"{message.content}\"\n\n" f"FONCTION : Extraire les horaires au format JSON strict.\n" f"FORMAT REQUIS :\n" f"`{{\"schedule\": {{\"mon\": [[\"HH:MM\", \"HH:MM\"]], \"tue\": [], ...}}, \"timezone\": \"Europe/Paris\"}}`\n" f"Note : Si l'Asset dit être libre tout le temps, laissez les jours vides []. S'il donne des heures, mettez-les.\n" f"IMPORTANT: Ne répondez QUE par le bloc JSON." ) try: payload = self.bot.llm.build_payload( prompt=prompt, system_prompt="Tu es un expert en extraction de données. Réponds uniquement en JSON.", force_json=True ) resp = await self.bot.llm.call_llama(payload) json_str = self.bot.llm.extract_json_actions(resp) if not json_str and resp.strip().startswith('{'): json_str = resp.strip() if json_str: data = json.loads(json_str) if isinstance(data, list): data = data[0] if "schedule" in data: self.assets[u_id] = { "name": message.author.display_name, "schedule": data.get("schedule", {}), "timezone": data.get("timezone", "Europe/Paris"), "last_update": time.time() } self._save_data(self.assets, ASSETS_DATA_PATH) del self.interview_states[u_id] await self.bot.messaging.send_embed( message.author, "INTÉGRATION RÉUSSIE", "Vos paramètres de disponibilité ont été synchronisés avec succès. Je vous contacterai selon ces critères.", color=discord.Color.green(), is_asset=True ) return True # Si on arrive ici, l'extraction a échoué logger.warning(f"Failed to extract schedule from: {resp}") await self.bot.messaging.send_embed( message.author, "ÉCHEC D'ACQUISITION", "Je n'ai pas pu parser vos horaires. Veuillez être plus précis (ex: 'Lundi de 8h à 12h') ou vérifier le format.", color=discord.Color.orange(), is_asset=True ) except Exception as e: logger.error(f"Failed to process interview response: {e}") await message.channel.send(f"◈ ERREUR CRITIQUE DURANT L'ANALYSE : {e}") return True def is_available(self, user_id: int, guild: discord.Guild) -> bool: """Check if an asset is available and physically present on the server.""" u_id = str(user_id) # 1. Server Presence & Permission Check (only if guild is provided) if guild: member = guild.get_member(user_id) if not member: return False # Verify they are ACTUALLY staff on this server if not check_is_admin(member.guild_permissions): # logger.warning(f"Asset {member.display_name} is present but lacks STAFF permissions on {guild.name}") return False # 2. Schedule Check asset = self.assets.get(u_id) if not asset: return True # Default available if unknown # Simple weekday/hour check (basic implementation) now = datetime.datetime.now() # Should use asset's timezone in real impl day = now.strftime("%a").lower()[:3] current_time = now.strftime("%H:%M") schedule = asset.get("schedule", {}).get(day, []) if not schedule: return True # Available if no specific "busy" blocks for start, end in schedule: if start <= current_time <= end: return False # Occupied return True async def dispatch_insight(self, insight: str, importance: int, guild: discord.Guild, context_data: dict): """Dispatch an insight to the best available asset.""" available_assets = [] for asset_id in self.bot.asset_ids: if self.is_available(asset_id, guild): available_assets.append(asset_id) if not available_assets: # Fallback to Admins if no asset available for admin_id in self.bot.admin_ids: admin = self.bot.get_user(admin_id) or await self.bot.fetch_user(admin_id) if admin: await self.bot.messaging.send_embed(admin, "DISPATCH FALLBACK: NO ASSETS AVAILABLE", insight, color=discord.Color.gold()) return # Target selection (Simple round-robin or first available for now) target_id = available_assets[0] target_user = self.bot.get_user(target_id) or await self.bot.fetch_user(target_id) if target_user: # Create Task ID and Data task_id = f"task_{int(time.time())}" self.tasks[task_id] = { "asset_id": target_id, "content": insight, "importance": importance, "created_at": time.time(), "deadline": time.time() + (60 * (11 - importance) * 5), "guild_id": guild.id if guild else None, "status": "PENDING" } self._save_data(self.tasks, TASKS_DATA_PATH) view = TaskAcceptView(task_id, self) # Création des champs d'information structurés fields = [ {"name": "👤 Sujet", "value": f"{context_data['author_mention']} ({context_data['author_name']})", "inline": True}, {"name": "📍 Salon", "value": context_data['channel_mention'], "inline": True}, {"name": "📝 Contenu", "value": context_data['content'], "inline": False} ] # Ajout du lien direct s'il existe if context_data.get('jump_url'): fields.append({"name": "🔗 Lien Direct", "value": f"[Accéder au message]({context_data['jump_url']})", "inline": False}) desc = ( f"**MISSION OPÉRATIONNELLE**\n" f"{insight}\n\n" f"*Veuillez confirmer la prise en charge pour verrouiller la tâche.*" ) await self.bot.messaging.send_embed( target_user, f"ASSIGNATION TACTIQUE (Priorité {importance}/10)", desc, color=discord.Color.blue(), is_asset=True, fields=fields ) # Send view separately await target_user.send(view=view) class TaskAcceptView(discord.ui.View): def __init__(self, task_id, dispatcher): super().__init__(timeout=None) self.task_id = task_id self.dispatcher = dispatcher @discord.ui.button(label="Accepter la tâche", style=discord.ButtonStyle.green) async def accept(self, interaction: discord.Interaction, button: discord.ui.Button): task = self.dispatcher.tasks.get(self.task_id) if task: task["status"] = "ACCEPTED" task["accepted_at"] = time.time() self.dispatcher._save_data(self.dispatcher.tasks, TASKS_DATA_PATH) button.disabled = True button.label = "Acceptée ✅" await interaction.response.edit_message(view=self) await interaction.followup.send("◈ Tâche verrouillée. Bonne intervention.", ephemeral=True) else: await interaction.response.send_message("Tâche introuvable ou expirée.", ephemeral=True)