import asyncio import os import shutil from typing import Any, Dict from sqlmodel import Session, select from core.docker_instance import docker_manager from core.settings import BOTS_WORKSPACE_ROOT from models.bot import BotInstance, BotMessage from models.platform import BotActivityEvent, BotRequestUsage from models.skill import BotSkillInstall from models.topic import TopicItem, TopicTopic from services.bot_service import ( _safe_float, _safe_int, read_bot_runtime_snapshot, resolve_bot_runtime_env_params, sync_bot_workspace_channels, ) from services.bot_storage_service import write_bot_env_params from services.cache_service import _invalidate_bot_detail_cache, _invalidate_bot_messages_cache from services.platform_service import record_activity_event from services.runtime_service import docker_callback, record_agent_loop_ready_warning def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance: bot = session.get(BotInstance, bot_id) if not bot: raise ValueError("Bot not found") return bot async def start_bot_instance(session: Session, bot_id: str) -> Dict[str, Any]: bot = _get_bot_or_404(session, bot_id) if not bool(getattr(bot, "enabled", True)): raise PermissionError("Bot is disabled. Enable it first.") sync_bot_workspace_channels(session, bot_id) runtime_snapshot = read_bot_runtime_snapshot(bot) env_params = resolve_bot_runtime_env_params(bot_id) write_bot_env_params(bot_id, env_params) success = docker_manager.start_bot( bot_id, image_tag=bot.image_tag, on_state_change=docker_callback, env_vars=env_params, cpu_cores=_safe_float(runtime_snapshot.get("cpu_cores"), 1.0), memory_mb=_safe_int(runtime_snapshot.get("memory_mb"), 1024), storage_gb=_safe_int(runtime_snapshot.get("storage_gb"), 10), ) if not success: bot.docker_status = "STOPPED" session.add(bot) session.commit() raise RuntimeError(f"Failed to start container with image {bot.image_tag}") actual_status = docker_manager.get_bot_status(bot_id) bot.docker_status = actual_status if actual_status != "RUNNING": session.add(bot) session.commit() _invalidate_bot_detail_cache(bot_id) raise RuntimeError("Bot container failed shortly after startup. Check bot logs/config.") asyncio.create_task(record_agent_loop_ready_warning(bot_id)) session.add(bot) record_activity_event(session, bot_id, "bot_started", channel="system", detail=f"Container started for {bot_id}") session.commit() _invalidate_bot_detail_cache(bot_id) return {"status": "started"} def stop_bot_instance(session: Session, bot_id: str) -> Dict[str, Any]: bot = _get_bot_or_404(session, bot_id) if not bool(getattr(bot, "enabled", True)): raise PermissionError("Bot is disabled. Enable it first.") docker_manager.stop_bot(bot_id) bot.docker_status = "STOPPED" session.add(bot) record_activity_event(session, bot_id, "bot_stopped", channel="system", detail=f"Container stopped for {bot_id}") session.commit() _invalidate_bot_detail_cache(bot_id) return {"status": "stopped"} def enable_bot_instance(session: Session, bot_id: str) -> Dict[str, Any]: bot = _get_bot_or_404(session, bot_id) bot.enabled = True session.add(bot) record_activity_event(session, bot_id, "bot_enabled", channel="system", detail=f"Bot {bot_id} enabled") session.commit() _invalidate_bot_detail_cache(bot_id) return {"status": "enabled", "enabled": True} def disable_bot_instance(session: Session, bot_id: str) -> Dict[str, Any]: bot = _get_bot_or_404(session, bot_id) docker_manager.stop_bot(bot_id) bot.enabled = False bot.docker_status = "STOPPED" if str(bot.current_state or "").upper() not in {"ERROR"}: bot.current_state = "IDLE" session.add(bot) record_activity_event(session, bot_id, "bot_disabled", channel="system", detail=f"Bot {bot_id} disabled") session.commit() _invalidate_bot_detail_cache(bot_id) return {"status": "disabled", "enabled": False} def deactivate_bot_instance(session: Session, bot_id: str) -> Dict[str, Any]: bot = _get_bot_or_404(session, bot_id) docker_manager.stop_bot(bot_id) bot.enabled = False bot.docker_status = "STOPPED" if str(bot.current_state or "").upper() not in {"ERROR"}: bot.current_state = "IDLE" session.add(bot) record_activity_event(session, bot_id, "bot_deactivated", channel="system", detail=f"Bot {bot_id} deactivated") session.commit() _invalidate_bot_detail_cache(bot_id) return {"status": "deactivated"} def delete_bot_instance(session: Session, bot_id: str, delete_workspace: bool = True) -> Dict[str, Any]: bot = _get_bot_or_404(session, bot_id) docker_manager.stop_bot(bot_id) messages = session.exec(select(BotMessage).where(BotMessage.bot_id == bot_id)).all() for row in messages: session.delete(row) topic_items = session.exec(select(TopicItem).where(TopicItem.bot_id == bot_id)).all() for row in topic_items: session.delete(row) topics = session.exec(select(TopicTopic).where(TopicTopic.bot_id == bot_id)).all() for row in topics: session.delete(row) usage_rows = session.exec(select(BotRequestUsage).where(BotRequestUsage.bot_id == bot_id)).all() for row in usage_rows: session.delete(row) activity_rows = session.exec(select(BotActivityEvent).where(BotActivityEvent.bot_id == bot_id)).all() for row in activity_rows: session.delete(row) skill_install_rows = session.exec(select(BotSkillInstall).where(BotSkillInstall.bot_id == bot_id)).all() for row in skill_install_rows: session.delete(row) session.delete(bot) session.commit() if delete_workspace: workspace_root = os.path.join(BOTS_WORKSPACE_ROOT, bot_id) if os.path.isdir(workspace_root): shutil.rmtree(workspace_root, ignore_errors=True) _invalidate_bot_detail_cache(bot_id) _invalidate_bot_messages_cache(bot_id) return {"status": "deleted", "workspace_deleted": bool(delete_workspace)}