Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion mesa_llm/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(
vision: float | None = None,
internal_state: list[str] | str | None = None,
step_prompt: str | None = None,
include_builtin_tools: bool = True,
):
super().__init__(model=model)

Expand All @@ -59,7 +60,7 @@ def __init__(
llm_model=llm_model,
)

self.tool_manager = ToolManager()
self.tool_manager = ToolManager(include_builtins=include_builtin_tools)
self.vision = vision
self.reasoning = reasoning(agent=self)
self.system_prompt = system_prompt
Expand Down
27 changes: 23 additions & 4 deletions mesa_llm/tools/tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,26 @@ class ToolManager:

instances: list["ToolManager"] = []

def __init__(self, extra_tools: dict[str, Callable] | None = None):
# start from everything that was decorated
def __init__(
self,
extra_tools: dict[str, Callable] | None = None,
include_builtins: bool = True,
):
"""
Args:
extra_tools: additional tool functions to register on this
instance (merged on top of globals when *include_builtins*
is True, or used as the sole tool set when False).
include_builtins: when True (default) the manager starts with
every globally-registered tool (move_one_step,
teleport_to_location, speak_to, …). Set to False to
create a manager with *only* the tools you explicitly
provide via *extra_tools*, preventing capability leakage
for agents that should not have spatial or communication
tools.
"""
ToolManager.instances.append(self)
self.tools = dict(_GLOBAL_TOOL_REGISTRY)
# allow per-agent overrides / reductions
self.tools = dict(_GLOBAL_TOOL_REGISTRY) if include_builtins else {}
if extra_tools:
self.tools.update(extra_tools)

Expand Down Expand Up @@ -87,6 +102,10 @@ def call(self, name: str, arguments: dict) -> str:
def has_tool(self, name: str) -> bool:
return name in self.tools

def remove_tool(self, name: str):
"""Remove a tool by name. Silently ignores missing tools."""
self.tools.pop(name, None)

async def _process_tool_call(
self, agent: "LLMAgent", tool_call: Any, index: int
) -> dict:
Expand Down
103 changes: 103 additions & 0 deletions tests/test_tools/test_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,3 +715,106 @@ def async_test_tool(agent, value: str) -> str:
assert len(result) == 1
assert result[0]["tool_call_id"] == "call_async"
assert "Async: hello" in result[0]["response"]

# ------------------------------------------------------------------
# Tests for include_builtins and remove_tool (issue #90)
# ------------------------------------------------------------------

def test_include_builtins_false_starts_empty(self):
"""ToolManager(include_builtins=False) should have no global tools."""

# Register some global tools first
@tool
def global_tool_a(agent, x: int) -> int:
"""Global tool A.

Args:
agent: Provided automatically.
x: Input.

Returns:
Output.
"""
return x

manager = ToolManager(include_builtins=False)
assert len(manager.tools) == 0
assert not manager.has_tool("global_tool_a")

def test_include_builtins_true_includes_global_tools(self):
"""Default ToolManager should include globally registered tools."""

@tool
def global_tool_b(agent, x: int) -> int:
"""Global tool B.

Args:
agent: Provided automatically.
x: Input.

Returns:
Output.
"""
return x

manager = ToolManager(include_builtins=True)
assert manager.has_tool("global_tool_b")

def test_include_builtins_false_with_extra_tools(self):
"""include_builtins=False with extra_tools should only have extras."""

@tool
def global_should_not_appear(agent, x: int) -> int:
"""Should not appear.

Args:
agent: Provided automatically.
x: Input.

Returns:
Output.
"""
return x

def custom_tool(agent, y: str) -> str:
return y

custom_tool.__tool_schema__ = {
"type": "function",
"function": {"name": "custom_tool"},
}

manager = ToolManager(
include_builtins=False,
extra_tools={"custom_tool": custom_tool},
)
assert manager.has_tool("custom_tool")
assert not manager.has_tool("global_should_not_appear")
assert len(manager.tools) == 1

def test_remove_tool(self):
"""remove_tool should remove a tool by name."""

@tool
def removable_tool(agent, x: int) -> int:
"""Removable tool.

Args:
agent: Provided automatically.
x: Input.

Returns:
Output.
"""
return x

manager = ToolManager()
assert manager.has_tool("removable_tool")

manager.remove_tool("removable_tool")
assert not manager.has_tool("removable_tool")

def test_remove_tool_missing_is_silent(self):
"""remove_tool should not raise on missing tool names."""
manager = ToolManager()
manager.remove_tool("nonexistent_tool") # should not raise
Loading