Rebase onto upstream (a4d95fd) #12

Closed
wylab wants to merge 144 commits from rebase-onto-upstream into main
2 changed files with 84 additions and 12 deletions
Showing only changes of commit d27db8168c - Show all commits
+27 -12
View File
@@ -46,24 +46,39 @@ class ToolRegistry:
raise ValueError(f"Tool {tool.name} has no schema method (to_params or to_schema)")
return definitions
async def execute(self, name: str, params: dict[str, Any]) -> str:
"""Execute a tool by name with given parameters."""
_HINT = "\n\n[Analyze the error above and try a different approach.]"
async def execute(self, name: str, params: dict[str, Any]) -> Any:
"""
Execute a tool by name with given parameters.
Supports both native Anthropic tools (via __call__) and function tools (via execute).
Args:
name: Tool name.
params: Tool parameters.
Returns:
Tool execution result (ToolResult, CLIResult, or string).
Raises:
KeyError: If tool not found.
"""
tool = self._tools.get(name)
if not tool:
return f"Error: Tool '{name}' not found. Available: {', '.join(self.tool_names)}"
return f"Error: Tool '{name}' not found"
try:
errors = tool.validate_params(params)
if errors:
return f"Error: Invalid parameters for tool '{name}': " + "; ".join(errors) + _HINT
result = await tool.execute(**params)
if isinstance(result, str) and result.startswith("Error"):
return result + _HINT
return result
# Duck typing - support both native and function tools
if hasattr(tool, 'to_params'):
# Native Anthropic tool - call directly via __call__, no validation needed
return await tool(**params)
else:
# Legacy function tool - validate then execute
errors = tool.validate_params(params)
if errors:
return f"Error: Invalid parameters for tool '{name}': " + "; ".join(errors)
return await tool.execute(**params)
except Exception as e:
return f"Error executing {name}: {str(e)}" + _HINT
return f"Error executing {name}: {str(e)}"
@property
def tool_names(self) -> list[str]:
+57
View File
@@ -0,0 +1,57 @@
"""Tests for registry execution of native tools."""
import pytest
import tempfile
from pathlib import Path
from nanobot.agent.tools.registry import ToolRegistry
from nanobot.agent.tools.anthropic import BashTool20250124, EditTool20250728
from nanobot.agent.tools.anthropic.base import ToolResult, CLIResult
@pytest.mark.asyncio
async def test_registry_executes_bash_tool():
"""Test registry can execute BashTool20250124 and returns ToolResult."""
registry = ToolRegistry()
registry.register(BashTool20250124())
result = await registry.execute("bash", {"command": "echo 'test'"})
assert isinstance(result, ToolResult)
assert result.output is not None
assert "test" in result.output
assert result.error is None
@pytest.mark.asyncio
async def test_registry_executes_edit_tool():
"""Test registry can execute EditTool20250728 and returns CLIResult."""
registry = ToolRegistry()
registry.register(EditTool20250728())
with tempfile.TemporaryDirectory() as tmpdir:
test_file = str(Path(tmpdir) / "test.txt")
result = await registry.execute("str_replace_editor", {
"command": "create",
"path": test_file,
"file_text": "Hello, world!"
})
assert isinstance(result, CLIResult)
assert "created" in result.output.lower() or "success" in result.output.lower()
assert Path(test_file).exists()
assert Path(test_file).read_text() == "Hello, world!"
@pytest.mark.asyncio
async def test_registry_mixed_tools():
"""Test registry can execute both native and function tools in same registry."""
registry = ToolRegistry()
# Register native tool
registry.register(BashTool20250124())
# Execute native tool
result = await registry.execute("bash", {"command": "echo 'native'"})
assert isinstance(result, ToolResult)
assert "native" in result.output