import asyncio from typing import Any, Awaitable, Callable, Dict, List, Optional from fastapi import HTTPException from sqlmodel import Session from models.bot import BotInstance from providers.provision.base import ProvisionProvider from providers.runtime.base import RuntimeProvider from services.platform_service import record_activity_event class LocalRuntimeProvider(RuntimeProvider): def __init__( self, *, docker_manager: Any, on_state_change: Callable[[str, dict], None], provision_provider: ProvisionProvider, read_runtime_snapshot: Callable[[BotInstance], Dict[str, Any]], resolve_env_params: Callable[[str], Dict[str, str]], write_env_store: Callable[[str, Dict[str, str]], None], invalidate_bot_cache: Callable[[str], None], record_agent_loop_ready_warning: Callable[[str], Awaitable[None]], safe_float: Callable[[Any, float], float], safe_int: Callable[[Any, int], int], ) -> None: self._docker_manager = docker_manager self._on_state_change = on_state_change self._provision_provider = provision_provider self._read_runtime_snapshot = read_runtime_snapshot self._resolve_env_params = resolve_env_params self._write_env_store = write_env_store self._invalidate_bot_cache = invalidate_bot_cache self._record_agent_loop_ready_warning = record_agent_loop_ready_warning self._safe_float = safe_float self._safe_int = safe_int async def start_bot(self, *, session: Session, bot: BotInstance) -> Dict[str, Any]: bot_id = str(bot.id or "").strip() if not bot_id: raise HTTPException(status_code=400, detail="Bot id is required") if not bool(getattr(bot, "enabled", True)): raise HTTPException(status_code=403, detail="Bot is disabled. Enable it first.") self._provision_provider.sync_bot_workspace(session=session, bot_id=bot_id) runtime_snapshot = self._read_runtime_snapshot(bot) env_params = self._resolve_env_params(bot_id) self._write_env_store(bot_id, env_params) success = self._docker_manager.start_bot( bot_id, image_tag=bot.image_tag, on_state_change=self._on_state_change, env_vars=env_params, cpu_cores=self._safe_float(runtime_snapshot.get("cpu_cores"), 1.0), memory_mb=self._safe_int(runtime_snapshot.get("memory_mb"), 1024), storage_gb=self._safe_int(runtime_snapshot.get("storage_gb"), 10), ) if not success: bot.docker_status = "STOPPED" session.add(bot) session.commit() raise HTTPException(status_code=500, detail=f"Failed to start container with image {bot.image_tag}") actual_status = self._docker_manager.get_bot_status(bot_id) bot.docker_status = actual_status if actual_status != "RUNNING": session.add(bot) session.commit() self._invalidate_bot_cache(bot_id) raise HTTPException( status_code=500, detail="Bot container failed shortly after startup. Check bot logs/config.", ) asyncio.create_task(self._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() self._invalidate_bot_cache(bot_id) return {"status": "started"} def stop_bot(self, *, session: Session, bot: BotInstance) -> Dict[str, Any]: bot_id = str(bot.id or "").strip() if not bot_id: raise HTTPException(status_code=400, detail="Bot id is required") if not bool(getattr(bot, "enabled", True)): raise HTTPException(status_code=403, detail="Bot is disabled. Enable it first.") self._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() self._invalidate_bot_cache(bot_id) return {"status": "stopped"} def deliver_command(self, *, bot_id: str, command: str, media: Optional[List[str]] = None) -> Optional[str]: success = self._docker_manager.send_command(bot_id, command, media=media) if success: return None return self._docker_manager.get_last_delivery_error(bot_id) or "command delivery failed" def get_recent_logs(self, *, bot_id: str, tail: int = 300) -> List[str]: return list(self._docker_manager.get_recent_logs(bot_id, tail=tail) or []) def ensure_monitor(self, *, bot_id: str) -> bool: return bool(self._docker_manager.ensure_monitor(bot_id, self._on_state_change)) def get_monitor_packets(self, *, bot_id: str, after_seq: int = 0, limit: int = 200) -> List[Dict[str, Any]]: return [] def get_runtime_status(self, *, bot_id: str) -> str: return str(self._docker_manager.get_bot_status(bot_id) or "STOPPED").upper() def get_resource_snapshot(self, *, bot_id: str) -> Dict[str, Any]: return dict(self._docker_manager.get_bot_resource_snapshot(bot_id) or {})