Beta/superviseur/dispatcher.py

235 lines
10 KiB
Python
Raw Permalink Normal View History

2026-02-06 22:23:20 +01:00
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__), "..", "assets_config.json")
TASKS_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "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 = {
"model": self.bot.ollama_model_fast,
"prompt": prompt,
"stream": False
}
resp = await self.bot.llm.call_ollama(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)