From 824a5b5171630b9caf5dd4222ae8437792cfbe99 Mon Sep 17 00:00:00 2001 From: Yoann Dumont Date: Fri, 17 Oct 2025 21:48:37 +0200 Subject: [PATCH 1/2] Implement asynchronous LLM selectors with error handling; fallback mechanism added for HackClub, OpenRouter, and IO Intelligence APIs. --- utils/llm_selector.py | 95 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/utils/llm_selector.py b/utils/llm_selector.py index 01df31e..ad1f68d 100644 --- a/utils/llm_selector.py +++ b/utils/llm_selector.py @@ -1,4 +1,5 @@ import logging +import asyncio from utils.config import logger_name, get_llm_selector_preprompt, CONFIG, API_ENDPOINTS_TEXT, MODELS_CONFIG_TEXT from dotenv import load_dotenv import os @@ -16,6 +17,69 @@ async def models(format): return "\n".join(f"- {k} : {v}" for k, v in models_dict.items()) else: return models_dict + + +async def hackclub_llm_selector(messages): + def _sync_request(): + response = requests.post( + "https://ai.hackclub.com/chat/completions", + headers={ + "Content-Type": "application/json", + }, + json={ + "model": "openai/gpt-oss-20b", + "messages": messages + } + ) + if response.status_code == 200: + return response.json()["choices"][0]["message"]["content"] + else: + raise Exception(f"HackClub API error: {response.status_code} {response.text}") + + return await asyncio.to_thread(_sync_request) + +async def io_intelligence_llm(messages): + def _sync_request(): + response = requests.post( + "https://api.intelligence.io.solutions/api/v1/chat/completions", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {os.getenv('IO_INTELLIGENCE_API_KEY')}" + }, + json={ + "model": "openai/gpt-oss-20b", + "messages": messages + } + ) + if response.status_code == 200: + return response.json()["choices"][0]["message"]["content"] + else: + raise Exception(f"IO Intelligence API error: {response.status_code} {response.text}") + + return await asyncio.to_thread(_sync_request) + + +async def openrouter_llm_selector(messages): + def _sync_request(): + response = requests.post( + "https://openrouter.ai/api/v1/chat/completions", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}" + }, + json={ + "model": "openai/gpt-oss-20b", + "messages": messages + } + ) + if response.status_code == 200: + return response.json()["choices"][0]["message"]["content"] + else: + raise Exception(f"OpenRouter API error: {response.status_code} {response.text}") + + return await asyncio.to_thread(_sync_request) + + async def llm_selector(input): @@ -25,23 +89,20 @@ async def llm_selector(input): {"role": "system", "content": get_llm_selector_preprompt() + models_text}, {"role": "user", "content": input} ] - - response = requests.post( - API_ENDPOINTS_TEXT.get("hackclub", "https://ai.hackclub.com/chat/completions"), - headers={ - "Content-Type": "application/json", - }, - json={ - "model": MODELS_CONFIG_TEXT.get("llm_selector", "openai/gpt-oss-20b"), - "messages": messages - } - ) - - if response.status_code == 200: - data = response.json() - text = data["choices"][0]["message"]["content"] - else: - raise Exception(f"API error: {response.status_code} {response.text}") + + try : + text = await hackclub_llm_selector(messages) + except Exception as e: + logger.error(f"HackClub LLM Selector failed: {e}. Falling back to OpenRouter LLM Selector.") + try: + text = await openrouter_llm_selector(messages) + except Exception as e2: + logger.error(f"OpenRouter LLM Selector failed: {e2}. Falling back to IO Intelligence LLM Selector.") + try: + text = await io_intelligence_llm(messages) + except Exception as e3: + logger.error(f"IO Intelligence LLM Selector also failed: {e3}. Using default model.") + return "cerebras/llama3.3-70b" model = await parse_llm_selection(text) return model From 9f5cecfb460b5c5de96eb747f45679b1d3acc186 Mon Sep 17 00:00:00 2001 From: Yoann Dumont Date: Fri, 17 Oct 2025 22:45:40 +0200 Subject: [PATCH 2/2] Enhance bot shutdown handling and implement graceful restart mechanism; add signal handling for shutdown events and monitor stop.json for commands. --- api/endpoints/image_gen.py | 1 + bots/admin_bot.py | 63 +++++----- bots/bot.py | 6 +- bots/chatgpt_bot.py | 6 +- bots/claude_bot.py | 6 +- bots/command_bot.py | 4 +- bots/deepseek_bot.py | 6 +- bots/evilgpt_bot.py | 6 +- bots/gemini_bot.py | 6 +- bots/glm_bot.py | 6 +- bots/grok_bot.py | 6 +- bots/kimi_bot.py | 6 +- bots/llama_bot.py | 6 +- bots/logger_bot.py | 32 ++++- bots/mistral_bot.py | 4 +- bots/perplexity_bot.py | 4 +- bots/phi_bot.py | 6 +- bots/qwen_bot.py | 4 +- commands/admin_commands/restart.py | 9 +- commands/admin_commands/stop.py | 8 +- main.py | 191 +++++++++++++++++++++++------ 21 files changed, 260 insertions(+), 126 deletions(-) diff --git a/api/endpoints/image_gen.py b/api/endpoints/image_gen.py index a3af6c1..8638be0 100644 --- a/api/endpoints/image_gen.py +++ b/api/endpoints/image_gen.py @@ -41,6 +41,7 @@ async def generate_image( logger.info(f"Prompt amélioré: {final_prompt[:100]}{'...' if len(final_prompt) > 100 else ''}") from utils.image_gen import generate_image + size = "1024x1024" logger.debug(f"Démarrage de la génération avec timeout de {REQUEST_TIMEOUT * 2}s") response = await asyncio.wait_for( diff --git a/bots/admin_bot.py b/bots/admin_bot.py index b24826b..e9f6839 100644 --- a/bots/admin_bot.py +++ b/bots/admin_bot.py @@ -12,45 +12,42 @@ load_dotenv() TOKEN = get_admin_bot_token() - intents = discord.Intents.default() - bot = commands.Bot(command_prefix="!", owner_ids=DEV_IDS, intents=intents) - supabase = get_supabase_client() logger = logging.getLogger(LOGGER_NAME) +purge_task = None + @bot.event async def on_ready(): + global purge_task activity = discord.CustomActivity(name="⚙️ Administrate AlphaLLM") await bot.change_presence(activity=activity, status=discord.Status.online) await bot.tree.sync() - while True: - await auto_purge() - await asyncio.sleep(60) + if purge_task is None or purge_task.done(): + purge_task = asyncio.create_task(purge_loop()) + +async def purge_loop(): + try: + while True: + await auto_purge() + await asyncio.sleep(60) + except asyncio.CancelledError: + pass async def auto_purge(): try: - # Utilise le premier développeur de la liste pour les DMs dev_user = await bot.fetch_user(DEV_IDS[0]) if DEV_IDS else None if not dev_user: return dm_channel = await dev_user.create_dm() - cutoff_time = discord.utils.utcnow() - datetime.timedelta(days=2.0) - deleted_count = 0 - async for message in dm_channel.history(limit=None, before=cutoff_time): try: await message.delete() - deleted_count += 1 - except discord.NotFound: - continue - except discord.HTTPException: + except (discord.NotFound, discord.HTTPException): continue - - except discord.HTTPException as e: - logger.error(f"Erreur lors de la purge : {e}") except Exception as e: logger.error(f"Erreur inattendue lors de l'auto-purge : {e}") @@ -60,35 +57,34 @@ async def clear_command(interaction: discord.Interaction): if not is_dev_id(interaction.user.id): await interaction.response.send_message("❌ Vous n'avez pas la permission d'utiliser cette commande.", ephemeral=True) return - await interaction.response.defer(ephemeral=True) - - # Utilise le premier développeur de la liste pour les DMs dev_user = await bot.fetch_user(DEV_IDS[0]) if DEV_IDS else None if not dev_user: await interaction.followup.send("❌ Aucun développeur configuré.", ephemeral=True) return dm_channel = await dev_user.create_dm() - deleted_count = 0 async for message in dm_channel.history(limit=None): try: await message.delete() deleted_count += 1 - except discord.NotFound: + except (discord.NotFound, discord.HTTPException): continue - except discord.HTTPException: - continue - - await interaction.followup.send(f"✅ {deleted_count} messages supprimés avec succès.", ephemeral=True) + await interaction.followup.send(f"{deleted_count} messages supprimés avec succès.", ephemeral=True) logger.info(f"Commande /clear exécutée : {deleted_count} messages supprimés") - - except discord.HTTPException as e: - logger.error(f"Erreur lors de la purge manuelle : {e}") - await interaction.followup.send(f"❌ Erreur lors de la purge : {e}", ephemeral=True) except Exception as e: logger.error(f"Erreur inattendue lors de la commande /clear : {e}") - await interaction.followup.send(f"❌ Erreur inattendue : {e}", ephemeral=True) + await interaction.followup.send(f"Erreur inattendue : {e}", ephemeral=True) + +async def close_bot(): + global purge_task + if purge_task and not purge_task.done(): + purge_task.cancel() + try: + await purge_task + except asyncio.CancelledError: + pass + await bot.close() async def run_admin_bot(): await setup_commands(bot, is_admin_bot=True) @@ -98,7 +94,6 @@ async def run_admin_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot.") - raise SystemExit(0) \ No newline at end of file + await close_bot() + logger.info("Arrêt du bot.") \ No newline at end of file diff --git a/bots/bot.py b/bots/bot.py index 98e7e88..a7b9bb2 100644 --- a/bots/bot.py +++ b/bots/bot.py @@ -91,7 +91,7 @@ async def run_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot.") \ No newline at end of file diff --git a/bots/chatgpt_bot.py b/bots/chatgpt_bot.py index 7d07ace..24ff112 100644 --- a/bots/chatgpt_bot.py +++ b/bots/chatgpt_bot.py @@ -36,7 +36,7 @@ async def run_chatgpt_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot ChatGPT.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot ChatGPT.") \ No newline at end of file diff --git a/bots/claude_bot.py b/bots/claude_bot.py index 6067786..319dab7 100644 --- a/bots/claude_bot.py +++ b/bots/claude_bot.py @@ -38,7 +38,7 @@ async def run_claude_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot Claude.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot Claude.") \ No newline at end of file diff --git a/bots/command_bot.py b/bots/command_bot.py index f7346d5..8a36a81 100644 --- a/bots/command_bot.py +++ b/bots/command_bot.py @@ -36,7 +36,7 @@ async def run_command_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: + if not bot.is_closed(): + await bot.close() logger.info("Arrêt du bot Command.") - raise SystemExit(0) \ No newline at end of file diff --git a/bots/deepseek_bot.py b/bots/deepseek_bot.py index 97c16d7..bc33193 100644 --- a/bots/deepseek_bot.py +++ b/bots/deepseek_bot.py @@ -36,7 +36,7 @@ async def run_deepseek_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot DeepSeek.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot DeepSeek.") \ No newline at end of file diff --git a/bots/evilgpt_bot.py b/bots/evilgpt_bot.py index 54d2968..f147f93 100644 --- a/bots/evilgpt_bot.py +++ b/bots/evilgpt_bot.py @@ -36,7 +36,7 @@ async def run_evilgpt_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot EvilGPT.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot EvilGPT.") \ No newline at end of file diff --git a/bots/gemini_bot.py b/bots/gemini_bot.py index 972f5c9..8a8b06e 100644 --- a/bots/gemini_bot.py +++ b/bots/gemini_bot.py @@ -36,7 +36,7 @@ async def run_gemini_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot Gemini.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot Gemini.") \ No newline at end of file diff --git a/bots/glm_bot.py b/bots/glm_bot.py index 2ecbccb..c0ffd29 100644 --- a/bots/glm_bot.py +++ b/bots/glm_bot.py @@ -36,7 +36,7 @@ async def run_glm_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot GLM.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot GLM.") \ No newline at end of file diff --git a/bots/grok_bot.py b/bots/grok_bot.py index ef62836..29f37e7 100644 --- a/bots/grok_bot.py +++ b/bots/grok_bot.py @@ -36,7 +36,7 @@ async def run_grok_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot Grok.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot Grok.") \ No newline at end of file diff --git a/bots/kimi_bot.py b/bots/kimi_bot.py index e589e4a..f3c8602 100644 --- a/bots/kimi_bot.py +++ b/bots/kimi_bot.py @@ -36,7 +36,7 @@ async def run_kimi_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot Kimi.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot Kimi.") \ No newline at end of file diff --git a/bots/llama_bot.py b/bots/llama_bot.py index eefaec6..66548fb 100644 --- a/bots/llama_bot.py +++ b/bots/llama_bot.py @@ -36,7 +36,7 @@ async def run_llama_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot Llama.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot Llama.") \ No newline at end of file diff --git a/bots/logger_bot.py b/bots/logger_bot.py index 718cffe..003f13b 100644 --- a/bots/logger_bot.py +++ b/bots/logger_bot.py @@ -17,14 +17,37 @@ logger_bot = commands.Bot(command_prefix=LOGGER_PREFIX, intents=intents) logger = setup_logging(logger_bot) +# Variable globale pour la tâche de purge +purge_task = None + @logger_bot.event async def on_ready(): + global purge_task activity = discord.CustomActivity(name="🎛️ Monitoring AlphaLLM") await logger_bot.change_presence(activity=activity) await logger_bot.tree.sync() - while True: - await auto_purge() - await asyncio.sleep(60) + + # Lance la tâche de purge automatique + if purge_task is None or purge_task.done(): + purge_task = asyncio.create_task(purge_loop()) + +async def purge_loop(): + try: + while True: + await auto_purge() + await asyncio.sleep(60) + except asyncio.CancelledError: + pass + +async def close_bot(): + global purge_task + if purge_task and not purge_task.done(): + purge_task.cancel() + try: + await purge_task + except asyncio.CancelledError: + pass + await logger_bot.close() async def auto_purge(): try: @@ -161,7 +184,6 @@ async def run_logger_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await logger_bot.close() finally: + await close_bot() logger.info("Arrêt du bot Logger.") - raise SystemExit(0) diff --git a/bots/mistral_bot.py b/bots/mistral_bot.py index 6b92d5c..b5bcb5c 100644 --- a/bots/mistral_bot.py +++ b/bots/mistral_bot.py @@ -36,7 +36,7 @@ async def run_mistral_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: + if not bot.is_closed(): + await bot.close() logger.info("Arrêt du bot Mistral.") - raise SystemExit(0) diff --git a/bots/perplexity_bot.py b/bots/perplexity_bot.py index 94cf9ea..693c205 100644 --- a/bots/perplexity_bot.py +++ b/bots/perplexity_bot.py @@ -36,7 +36,7 @@ async def run_perplexity_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: + if not bot.is_closed(): + await bot.close() logger.info("Arrêt du bot Perplexity.") - raise SystemExit(0) diff --git a/bots/phi_bot.py b/bots/phi_bot.py index f3ab8c0..87515af 100644 --- a/bots/phi_bot.py +++ b/bots/phi_bot.py @@ -36,7 +36,7 @@ async def run_phi_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: - logger.info("Arrêt du bot Phi.") - raise SystemExit(0) \ No newline at end of file + if not bot.is_closed(): + await bot.close() + logger.info("Arrêt du bot Phi.") \ No newline at end of file diff --git a/bots/qwen_bot.py b/bots/qwen_bot.py index 5e9624e..ef3a04b 100644 --- a/bots/qwen_bot.py +++ b/bots/qwen_bot.py @@ -36,7 +36,7 @@ async def run_qwen_bot(): logger.error(f"Erreur de connexion : {e}") except Exception as e: logger.error(f"Erreur inattendue : {e}") - await bot.close() finally: + if not bot.is_closed(): + await bot.close() logger.info("Arrêt du bot Qwen.") - raise SystemExit(0) diff --git a/commands/admin_commands/restart.py b/commands/admin_commands/restart.py index 2a124f4..283f794 100644 --- a/commands/admin_commands/restart.py +++ b/commands/admin_commands/restart.py @@ -24,8 +24,7 @@ async def restart(interaction: discord.Interaction): await interaction.followup.send("🛑 Redémarrage complet du bot...", ephemeral=True) logger.info("Demande de redémarrage reçue") - if bot.is_closed(): - return - - await bot.close() - sys.exit(0) + with open("stop.json", "w") as f: + json.dump({"COMMAND": "RESTART", "timestamp": datetime.datetime.now().isoformat()}, f) + + logger.info("Fichier stop.json créé, le processus principal va redémarrer le bot.") diff --git a/commands/admin_commands/stop.py b/commands/admin_commands/stop.py index 62c34b6..d67a56c 100644 --- a/commands/admin_commands/stop.py +++ b/commands/admin_commands/stop.py @@ -26,9 +26,5 @@ async def stop(interaction: discord.Interaction): with open("stop.json", "w") as f: json.dump({"COMMAND": "STOP", "timestamp": datetime.datetime.now().isoformat()}, f) - - if bot.is_closed(): - return - - await bot.close() - sys.exit(0) + + logger.info("Fichier stop.json créé, le processus principal va arrêter le bot.") diff --git a/main.py b/main.py index 7c67521..9e50cf4 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ import datetime import sys import os +import signal from bots.bot import run_bot from bots.admin_bot import run_admin_bot from bots.logger_bot import run_logger_bot @@ -29,28 +30,118 @@ logger = logging.getLogger(LOGGER_NAME) logger.setLevel(get_logging_level()) +# Variable globale pour gérer l'arrêt propre +shutdown_event = asyncio.Event() +restart_requested = False + +def handle_shutdown_signal(signum, frame): + """Gestionnaire de signal pour arrêt propre""" + global restart_requested + logger.debug(f"Signal {signum} reçu, arrêt en cours...") + + # Vérifie si c'est un redémarrage ou un arrêt + command = check_restart_command() + logger.debug(f"Commande lue depuis stop.json: {command}") + restart_requested = (command == "RESTART") + logger.debug(f"Redémarrage demandé: {restart_requested}") + + # Déclenche l'événement d'arrêt + try: + loop = asyncio.get_running_loop() + logger.debug("Event loop trouvée, déclenchement de shutdown_event") + loop.call_soon_threadsafe(shutdown_event.set) + logger.debug("shutdown_event.set() appelé") + except RuntimeError as e: + logger.error(f"Impossible de récupérer l'event loop: {e}") + +def check_restart_command(): + """Vérifie si un redémarrage ou arrêt a été demandé""" + try: + with open("stop.json", "r") as f: + data = json.load(f) + command = data.get("COMMAND") + timestamp = datetime.datetime.fromisoformat(data["timestamp"]) + + # Vérifie que la commande n'est pas trop ancienne (évite les boucles) + if datetime.datetime.now() - timestamp > datetime.timedelta(minutes=1): + os.remove("stop.json") + return None + + return command + except (FileNotFoundError, json.JSONDecodeError, KeyError): + return None + async def main(): + # Configure les gestionnaires de signaux + signal.signal(signal.SIGTERM, handle_shutdown_signal) + signal.signal(signal.SIGINT, handle_shutdown_signal) + + # Crée une tâche pour surveiller le fichier stop.json + async def monitor_stop_file(): + """Surveille le fichier stop.json pour détecter les demandes d'arrêt""" + while not shutdown_event.is_set(): + try: + if os.path.exists("stop.json"): + command = check_restart_command() + if command in ["STOP", "RESTART"]: + logger.debug(f"Commande {command} détectée dans stop.json") + # Déclenche l'arrêt + os.kill(os.getpid(), signal.SIGTERM) + break + except Exception as e: + logger.error(f"Erreur lors de la surveillance du fichier stop.json: {e}") + await asyncio.sleep(1) + + # Wrapper pour arrêter les bots quand shutdown_event est déclenché + async def run_with_shutdown(coro, name="task"): + """Execute une coroutine et l'annule quand shutdown_event est set""" + task = asyncio.create_task(coro) + + # Attend soit la fin de la tâche, soit le shutdown + done, pending = await asyncio.wait( + [task, asyncio.create_task(shutdown_event.wait())], + return_when=asyncio.FIRST_COMPLETED + ) + + # Si shutdown_event est déclenché, annule la tâche + if shutdown_event.is_set(): + logger.debug(f"Arrêt de la tâche: {name}") + task.cancel() + try: + # Laisse 2 secondes pour terminer proprement + await asyncio.wait_for(task, timeout=2.0) + except asyncio.CancelledError: + logger.debug(f"Tâche {name} annulée proprement") + except asyncio.TimeoutError: + logger.warning(f"Tâche {name} n'a pas pu se terminer dans le délai") + except Exception as e: + logger.error(f"Erreur lors de l'arrêt de {name}: {e}") + + return task.result() if task.done() and not task.cancelled() else None + try: await asyncio.gather( - start_api_async(), - ping_https_server(API_URL), - run_bot(), - run_admin_bot(), - run_logger_bot(), - run_mistral_bot(), - run_gemini_bot(), - run_evilgpt_bot(), - run_llama_bot(), - run_chatgpt_bot(), - run_deepseek_bot(), - run_grok_bot(), - run_perplexity_bot(), - run_qwen_bot(), - run_claude_bot(), - run_phi_bot(), - run_kimi_bot(), - run_glm_bot(), - run_command_bot() + monitor_stop_file(), + run_with_shutdown(start_api_async(), "API"), + run_with_shutdown(ping_https_server(API_URL), "Ping HTTPS"), + run_with_shutdown(run_bot(), "Bot principal"), + run_with_shutdown(run_admin_bot(), "Admin Bot"), + run_with_shutdown(run_logger_bot(), "Logger Bot"), + run_with_shutdown(run_mistral_bot(), "Mistral Bot"), + run_with_shutdown(run_gemini_bot(), "Gemini Bot"), + run_with_shutdown(run_evilgpt_bot(), "EvilGPT Bot"), + run_with_shutdown(run_llama_bot(), "Llama Bot"), + run_with_shutdown(run_chatgpt_bot(), "ChatGPT Bot"), + run_with_shutdown(run_deepseek_bot(), "DeepSeek Bot"), + run_with_shutdown(run_grok_bot(), "Grok Bot"), + run_with_shutdown(run_perplexity_bot(), "Perplexity Bot"), + run_with_shutdown(run_qwen_bot(), "Qwen Bot"), + run_with_shutdown(run_claude_bot(), "Claude Bot"), + run_with_shutdown(run_phi_bot(), "Phi Bot"), + run_with_shutdown(run_kimi_bot(), "Kimi Bot"), + run_with_shutdown(run_glm_bot(), "GLM Bot"), + run_with_shutdown(run_command_bot(), "Command Bot"), + return_exceptions=True ) except (SystemExit, KeyboardInterrupt): @@ -58,30 +149,60 @@ async def main(): except Exception as e: logger.error(f"Erreur non gérée : {str(e)}") finally: - tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] - if tasks: - for task in tasks: + logger.debug("Entrée dans le bloc finally de main()") + # Annule et attend toutes les tâches restantes pour éviter les warnings + pending = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] + logger.debug(f"Nombre de tâches en attente: {len(pending)}") + if pending: + for task in pending: task.cancel() try: - await asyncio.gather(*tasks, return_exceptions=True) + logger.debug("Attente de la fin des tâches annulées...") + await asyncio.gather(*pending, return_exceptions=True) + logger.debug("Toutes les tâches ont été annulées et attendues.") except Exception as e: logger.error(f"Erreur lors du nettoyage des tâches: {str(e)}") logger.info("Nettoyage terminé.") if __name__ == "__main__": + # Vérifie si un arrêt a été demandé au démarrage + command = check_restart_command() + if command == "STOP": + logger.info("Arrêt demandé via stop.json. Le bot ne démarrera pas.") + try: + os.remove("stop.json") + except Exception: + pass + sys.exit(0) + + # Supprime le fichier stop.json s'il existe pour éviter les conflits try: - with open("stop.json", "r") as f: - data = json.load(f) - timestamp = datetime.datetime.fromisoformat(data["timestamp"]) - if datetime.datetime.now() - timestamp < datetime.timedelta(minutes=1): - print("Le bot a été arrêté récemment. Redémarrage annulé.") - sys.exit(0) - else: - os.remove("stop.json") - except FileNotFoundError: - pass - + if os.path.exists("stop.json"): + os.remove("stop.json") + except Exception as e: + logger.warning(f"Impossible de supprimer stop.json: {e}") + + # Lance le bot try: + logger.debug("Lancement de asyncio.run(main())...") asyncio.run(main()) + logger.debug("asyncio.run(main()) terminé.") except KeyboardInterrupt: - logger.info("Interruption manuelle - Arrêt du programme.") + logger.debug("Interruption manuelle - Arrêt du programme.") + except Exception as e: + logger.error(f"Erreur fatale: {e}") + sys.exit(1) + + # Vérifie après la sortie complète si un redémarrage est demandé + logger.debug(f"Vérification du redémarrage: restart_requested = {restart_requested}") + if restart_requested: + logger.info("Redémarrage demandé. Relance du processus...") + try: + os.remove("stop.json") + except Exception: + pass + # Relance le processus + logger.debug(f"Exécution de os.execv({sys.executable}, {[sys.executable] + sys.argv})") + os.execv(sys.executable, [sys.executable] + sys.argv) + + logger.info("Arrêt complet du bot.")