Beta/core/dispatcher.py

235 lines
10 KiB
Python
Raw Permalink Normal View History

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)