Source code for tool_loader
"""Auto-discover and load tools from a directory of Python scripts.
Each ``.py`` file in the tools directory must expose **either**:
**Single-tool format** (one tool per file):
* ``TOOL_NAME`` -- unique name for the tool (str).
* ``TOOL_DESCRIPTION`` -- human-readable description (str).
* ``TOOL_PARAMETERS`` -- JSON Schema ``object`` for accepted args (dict).
* ``async def run(**kwargs) -> str`` -- the tool handler.
**Multi-tool format** (multiple tools per file):
* ``TOOLS`` -- a list of dicts, each with keys:
``name``, ``description``, ``parameters``, ``handler``.
Malformed files are logged as warnings but do **not** prevent the rest of
the tools from loading.
"""
from __future__ import annotations
import importlib.util
import logging
import sys
from pathlib import Path
from tools import ToolDefinition, ToolRegistry
logger = logging.getLogger(__name__)
_REQUIRED_ATTRS = ("TOOL_NAME", "TOOL_DESCRIPTION", "TOOL_PARAMETERS", "run")
[docs]
def load_tools(directory: str | Path, registry: ToolRegistry) -> None:
"""Scan *directory* for ``.py`` tool scripts and register them.
Parameters
----------
directory:
Path to the tools directory (e.g. ``"tools"``).
registry:
The :class:`ToolRegistry` to register discovered tools into.
"""
tools_path = Path(directory)
if not tools_path.is_dir():
logger.warning(
"Tools directory does not exist: %s – skipped",
tools_path,
)
return
for py_file in sorted(tools_path.glob("*.py")):
if py_file.name.startswith("_"):
continue # skip __init__.py, __pycache__ artefacts, etc.
module_name = f"_tool_{py_file.stem}"
try:
spec = importlib.util.spec_from_file_location(module_name, py_file)
if spec is None or spec.loader is None:
logger.warning(
"Could not create module spec for %s",
py_file,
)
continue
canonical = f"tools.{py_file.stem}"
if canonical in sys.modules:
module = sys.modules[canonical]
sys.modules[module_name] = module # alias only, no re-exec
else:
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
# --- Multi-tool format: TOOLS list ---------------------------
tools_list = getattr(module, "TOOLS", None)
if tools_list and isinstance(tools_list, list):
count = 0
for entry in tools_list:
t_name = entry.get("name")
t_desc = entry.get("description")
t_params = entry.get("parameters")
t_handler = entry.get("handler")
valid = (
t_name and t_desc
and t_params is not None
and callable(t_handler)
)
if not valid:
logger.warning(
"Tool %s: malformed TOOLS entry %r – skip",
py_file.name, entry.get("name", "?"),
)
continue
registry._tools[t_name] = ToolDefinition(
name=t_name,
description=t_desc,
parameters=t_params,
handler=t_handler,
no_background=bool(entry.get("no_background")),
allow_repeat=bool(entry.get("allow_repeat")),
)
count += 1
logger.info(
"Loaded %d tool(s) from %s (multi-tool)",
count, py_file.name,
)
continue
# --- Single-tool format --------------------------------------
missing = [
a for a in _REQUIRED_ATTRS
if not hasattr(module, a)
]
if missing:
logger.warning(
"Tool %s missing attrs: %s – skip",
py_file.name,
", ".join(missing),
)
continue
handler = getattr(module, "run")
if not callable(handler):
logger.warning(
"Tool %s: 'run' not callable – skip",
py_file.name,
)
continue
tool_name: str = getattr(module, "TOOL_NAME")
tool_desc: str = getattr(module, "TOOL_DESCRIPTION")
tool_params: dict = getattr(module, "TOOL_PARAMETERS")
no_bg = bool(getattr(module, "TOOL_NO_BACKGROUND", False))
allow_repeat = bool(getattr(module, "TOOL_ALLOW_REPEAT", False))
registry._tools[tool_name] = ToolDefinition(
name=tool_name,
description=tool_desc,
parameters=tool_params,
handler=handler,
no_background=no_bg,
allow_repeat=allow_repeat,
)
logger.info(
"Loaded tool: %s (%s)", tool_name, py_file.name,
)
except Exception:
logger.exception(
"Failed to load tool from %s", py_file.name,
)
logger.info(
"Tool loading complete – %d tool(s) registered",
len(registry),
)