"""Thin adapters between nanobot agents and the vendored swarms runtime.""" from __future__ import annotations import asyncio import sys from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from pathlib import Path from typing import Any from nanobot.agent.agent_registry import AgentDescriptor from nanobot.agent.run_result import AgentRunResult MemberRunner = Callable[[AgentDescriptor, str, str, list[str]], Awaitable[AgentRunResult]] def _candidate_swarms_roots() -> list[Path]: """Return likely vendored swarms paths across source and packaged layouts.""" module_path = Path(__file__).resolve() candidates = [ module_path.parents[2] / "third_party" / "swarms", Path("/opt/app/backend/third_party/swarms"), Path("/app/third_party/swarms"), Path.cwd() / "third_party" / "swarms", Path.cwd() / "backend" / "third_party" / "swarms", ] unique: list[Path] = [] seen: set[str] = set() for candidate in candidates: key = str(candidate) if key in seen: continue seen.add(key) unique.append(candidate) return unique def ensure_swarms_importable() -> None: """Put the vendored swarms checkout on `sys.path` if needed.""" for swarms_root in _candidate_swarms_roots(): if swarms_root.exists() and str(swarms_root) not in sys.path: sys.path.insert(0, str(swarms_root)) return def load_swarms_runtime() -> dict[str, Any]: """Lazy-load swarms classes without making package import fragile.""" ensure_swarms_importable() from swarms import AutoSwarmBuilder # type: ignore from swarms.structs.groupchat import GroupChat # type: ignore from swarms.structs.swarm_router import SwarmRouter # type: ignore return { "AutoSwarmBuilder": AutoSwarmBuilder, "GroupChat": GroupChat, "SwarmRouter": SwarmRouter, } def __getattr__(name: str) -> Any: if name in {"AutoSwarmBuilder", "GroupChat", "SwarmRouter"}: return load_swarms_runtime()[name] raise AttributeError(name) def safe_swarms_name(agent_id: str) -> str: """Return a GroupChat-friendly ASCII-ish name for @mentions.""" normalized = "".join(ch if ch.isalnum() else "_" for ch in str(agent_id or "agent")) normalized = normalized.strip("_") or "agent" return f"agent_{normalized}" @dataclass(eq=False) class NanobotAgentAdapter: """Callable wrapper that lets swarms invoke a nanobot agent descriptor.""" descriptor: AgentDescriptor run_id: str loop: asyncio.AbstractEventLoop member_runner: MemberRunner skills: list[str] results: list[AgentRunResult] = field(default_factory=list, init=False) def __post_init__(self) -> None: self.agent_name = safe_swarms_name(self.descriptor.id) self.name = self.agent_name self.system_prompt = self.descriptor.system_prompt or self.descriptor.description self.__name__ = self.agent_name def __call__(self, conversation_context: str) -> str: return self.run(conversation_context) def run(self, task: str, *args: Any, **kwargs: Any) -> str: delegated_task = self._task_with_skills(task) future = asyncio.run_coroutine_threadsafe( self.member_runner(self.descriptor, delegated_task, self.run_id, list(self.skills)), self.loop, ) result = future.result(timeout=300) self.results.append(result) if result.status != "ok": return f"Error from {self.agent_name}: {result.summary}" return result.summary def _task_with_skills(self, conversation_context: str) -> str: if not self.skills: return conversation_context return ( "Required skills for this delegated team member:\n" f"{', '.join(self.skills)}\n\n" "Swarms conversation context:\n" f"{conversation_context}" ).strip()