dashboard-nanobot/backend/providers/runtime/local.py

118 lines
5.2 KiB
Python

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_activity_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 {})