mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 16:38:49 +00:00
133 lines
5.4 KiB
Python
133 lines
5.4 KiB
Python
"""Tool discovery and registration via package scanning."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import pkgutil
|
|
from importlib.metadata import entry_points
|
|
from typing import Any
|
|
|
|
from loguru import logger
|
|
|
|
from nanobot.agent.tools.base import Tool
|
|
from nanobot.agent.tools.registry import ToolRegistry
|
|
|
|
_SKIP_MODULES = frozenset({
|
|
"base", "schema", "registry", "context", "loader", "config",
|
|
"file_state", "sandbox", "mcp", "__init__", "runtime_state",
|
|
})
|
|
|
|
|
|
class ToolLoader:
|
|
def __init__(self, package: Any = None, *, test_classes: list[type[Tool]] | None = None):
|
|
if package is None:
|
|
import nanobot.agent.tools as _pkg
|
|
package = _pkg
|
|
self._package = package
|
|
self._test_classes = test_classes
|
|
self._discovered: list[type[Tool]] | None = None
|
|
self._plugins: dict[str, type[Tool]] | None = None
|
|
|
|
def discover(self) -> list[type[Tool]]:
|
|
"""Discover concrete tools that should be registered automatically."""
|
|
if self._test_classes is not None:
|
|
return list(self._test_classes)
|
|
if self._discovered is not None:
|
|
return self._discovered
|
|
self._discovered = self._discover_package_tools(include_non_discoverable=False)
|
|
return self._discovered
|
|
|
|
def _discover_package_tools(self, *, include_non_discoverable: bool) -> list[type[Tool]]:
|
|
seen: set[int] = set()
|
|
results: list[type[Tool]] = []
|
|
for _importer, module_name, _ispkg in pkgutil.iter_modules(self._package.__path__):
|
|
if module_name.startswith("_") or module_name in _SKIP_MODULES:
|
|
continue
|
|
try:
|
|
module = importlib.import_module(f".{module_name}", self._package.__name__)
|
|
except Exception:
|
|
logger.exception("Failed to import tool module: %s", module_name)
|
|
continue
|
|
for attr_name in dir(module):
|
|
attr = getattr(module, attr_name)
|
|
if (
|
|
isinstance(attr, type)
|
|
and issubclass(attr, Tool)
|
|
and attr is not Tool
|
|
and not attr_name.startswith("_")
|
|
and not getattr(attr, "__abstractmethods__", None)
|
|
and (include_non_discoverable or getattr(attr, "_plugin_discoverable", True))
|
|
and id(attr) not in seen
|
|
):
|
|
seen.add(id(attr))
|
|
results.append(attr)
|
|
results.sort(key=lambda cls: cls.__name__)
|
|
return results
|
|
|
|
def discover_config_classes(self) -> list[type[Tool]]:
|
|
"""Discover tool classes that declare owned config models."""
|
|
classes = self._discover_package_tools(include_non_discoverable=True)
|
|
classes.extend(self._discover_plugins().values())
|
|
return [
|
|
cls for cls in classes
|
|
if getattr(cls, "config_key", "") and cls.config_cls() is not None
|
|
]
|
|
|
|
def _discover_plugins(self) -> dict[str, type[Tool]]:
|
|
"""Discover external tool plugins registered via entry_points."""
|
|
if self._plugins is not None:
|
|
return self._plugins
|
|
plugins: dict[str, type[Tool]] = {}
|
|
try:
|
|
eps = entry_points(group="nanobot.tools")
|
|
except Exception:
|
|
return plugins
|
|
for ep in eps:
|
|
try:
|
|
cls = ep.load()
|
|
if (
|
|
isinstance(cls, type)
|
|
and issubclass(cls, Tool)
|
|
and not getattr(cls, "__abstractmethods__", None)
|
|
and getattr(cls, "_plugin_discoverable", True)
|
|
):
|
|
plugins[ep.name] = cls
|
|
except Exception:
|
|
logger.exception("Failed to load tool plugin: %s", ep.name)
|
|
self._plugins = plugins
|
|
return plugins
|
|
|
|
def load(self, ctx: Any, registry: ToolRegistry, *, scope: str = "core") -> list[str]:
|
|
from nanobot.agent.tools.config import tool_config
|
|
|
|
registered: list[str] = []
|
|
builtin_names: set[str] = set()
|
|
sources = [(self.discover(), False), (self._discover_plugins().values(), True)]
|
|
for source, is_plugin_source in sources:
|
|
for tool_cls in source:
|
|
cls_label = tool_cls.__name__
|
|
try:
|
|
if scope not in getattr(tool_cls, "_scopes", {"core"}):
|
|
continue
|
|
tool_config(ctx.config, tool_cls)
|
|
if not tool_cls.enabled(ctx):
|
|
continue
|
|
tool = tool_cls.create(ctx)
|
|
if registry.has(tool.name):
|
|
if is_plugin_source and tool.name in builtin_names:
|
|
logger.warning(
|
|
"Plugin %s skipped: conflicts with built-in tool %s",
|
|
cls_label, tool.name,
|
|
)
|
|
continue
|
|
logger.warning(
|
|
"Tool name collision: %s from %s overwrites existing",
|
|
tool.name, cls_label,
|
|
)
|
|
registry.register(tool)
|
|
registered.append(tool.name)
|
|
if not is_plugin_source:
|
|
builtin_names.add(tool.name)
|
|
except Exception:
|
|
logger.exception("Failed to register tool: %s", cls_label)
|
|
return registered
|