mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-30 06:45:55 +00:00
get_definitions() sorts tools on every LLM iteration for prompt cache stability. Cache the sorted result and invalidate on register/unregister so the sort only runs when the tool set actually changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
2.9 KiB
Python
104 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from nanobot.agent.tools.base import Tool
|
|
from nanobot.agent.tools.registry import ToolRegistry
|
|
|
|
|
|
class _FakeTool(Tool):
|
|
def __init__(self, name: str):
|
|
self._name = name
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._name
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return f"{self._name} tool"
|
|
|
|
@property
|
|
def parameters(self) -> dict[str, Any]:
|
|
return {"type": "object", "properties": {}}
|
|
|
|
async def execute(self, **kwargs: Any) -> Any:
|
|
return kwargs
|
|
|
|
|
|
def _tool_names(definitions: list[dict[str, Any]]) -> list[str]:
|
|
names: list[str] = []
|
|
for definition in definitions:
|
|
fn = definition.get("function", {})
|
|
names.append(fn.get("name", ""))
|
|
return names
|
|
|
|
|
|
def test_get_definitions_orders_builtins_then_mcp_tools() -> None:
|
|
registry = ToolRegistry()
|
|
registry.register(_FakeTool("mcp_git_status"))
|
|
registry.register(_FakeTool("write_file"))
|
|
registry.register(_FakeTool("mcp_fs_list"))
|
|
registry.register(_FakeTool("read_file"))
|
|
|
|
assert _tool_names(registry.get_definitions()) == [
|
|
"read_file",
|
|
"write_file",
|
|
"mcp_fs_list",
|
|
"mcp_git_status",
|
|
]
|
|
|
|
|
|
def test_prepare_call_read_file_rejects_non_object_params_with_actionable_hint() -> None:
|
|
registry = ToolRegistry()
|
|
registry.register(_FakeTool("read_file"))
|
|
|
|
tool, params, error = registry.prepare_call("read_file", ["foo.txt"])
|
|
|
|
assert tool is None
|
|
assert params == ["foo.txt"]
|
|
assert error is not None
|
|
assert "must be a JSON object" in error
|
|
assert "Use named parameters" in error
|
|
|
|
|
|
def test_prepare_call_other_tools_keep_generic_object_validation() -> None:
|
|
registry = ToolRegistry()
|
|
registry.register(_FakeTool("grep"))
|
|
|
|
tool, params, error = registry.prepare_call("grep", ["TODO"])
|
|
|
|
assert tool is not None
|
|
assert params == ["TODO"]
|
|
assert error == "Error: Invalid parameters for tool 'grep': parameters must be an object, got list"
|
|
|
|
|
|
def test_get_definitions_returns_cached_result() -> None:
|
|
registry = ToolRegistry()
|
|
registry.register(_FakeTool("read_file"))
|
|
first = registry.get_definitions()
|
|
assert registry._cached_definitions is not None
|
|
second = registry.get_definitions()
|
|
assert first == second
|
|
|
|
|
|
def test_register_invalidates_cache() -> None:
|
|
registry = ToolRegistry()
|
|
registry.register(_FakeTool("read_file"))
|
|
first = registry.get_definitions()
|
|
registry.register(_FakeTool("write_file"))
|
|
second = registry.get_definitions()
|
|
assert first is not second
|
|
assert len(second) == 2
|
|
|
|
|
|
def test_unregister_invalidates_cache() -> None:
|
|
registry = ToolRegistry()
|
|
registry.register(_FakeTool("read_file"))
|
|
registry.register(_FakeTool("write_file"))
|
|
first = registry.get_definitions()
|
|
registry.unregister("write_file")
|
|
second = registry.get_definitions()
|
|
assert first is not second
|
|
assert len(second) == 1
|