refactor: extract tool hint formatting to utils/tool_hints.py

- Move _tool_hint implementation from loop.py to nanobot/utils/tool_hints.py
- Keep thin delegation in AgentLoop._tool_hint for backward compat
- Update test imports to test format_tool_hints directly

Made-with: Cursor
This commit is contained in:
Xubin Ren
2026-04-07 15:15:07 +08:00
committed by Xubin Ren
parent 3e3a7654f8
commit 82dec12f66
3 changed files with 156 additions and 137 deletions
+35 -30
View File
@@ -1,6 +1,6 @@
"""Tests for AgentLoop._tool_hint() formatting."""
"""Tests for tool hint formatting (nanobot.utils.tool_hints)."""
from nanobot.agent.loop import AgentLoop
from nanobot.utils.tool_hints import format_tool_hints
from nanobot.providers.base import ToolCallRequest
@@ -8,51 +8,56 @@ def _tc(name: str, args) -> ToolCallRequest:
return ToolCallRequest(id="c1", name=name, arguments=args)
def _hint(calls):
"""Shortcut for format_tool_hints."""
return format_tool_hints(calls)
class TestToolHintKnownTools:
"""Test registered tool types produce correct formatted output."""
def test_read_file_short_path(self):
result = AgentLoop._tool_hint([_tc("read_file", {"path": "foo.txt"})])
result = _hint([_tc("read_file", {"path": "foo.txt"})])
assert result == 'read foo.txt'
def test_read_file_long_path(self):
result = AgentLoop._tool_hint([_tc("read_file", {"path": "/home/user/.local/share/uv/tools/nanobot/agent/loop.py"})])
result = _hint([_tc("read_file", {"path": "/home/user/.local/share/uv/tools/nanobot/agent/loop.py"})])
assert "loop.py" in result
assert "read " in result
def test_write_file_shows_path_not_content(self):
result = AgentLoop._tool_hint([_tc("write_file", {"path": "docs/api.md", "content": "# API Reference\n\nLong content..."})])
result = _hint([_tc("write_file", {"path": "docs/api.md", "content": "# API Reference\n\nLong content..."})])
assert result == "write docs/api.md"
def test_edit_shows_path(self):
result = AgentLoop._tool_hint([_tc("edit", {"file_path": "src/main.py", "old_string": "x", "new_string": "y"})])
result = _hint([_tc("edit", {"file_path": "src/main.py", "old_string": "x", "new_string": "y"})])
assert "main.py" in result
assert "edit " in result
def test_glob_shows_pattern(self):
result = AgentLoop._tool_hint([_tc("glob", {"pattern": "**/*.py", "path": "src"})])
result = _hint([_tc("glob", {"pattern": "**/*.py", "path": "src"})])
assert result == 'glob "**/*.py"'
def test_grep_shows_pattern(self):
result = AgentLoop._tool_hint([_tc("grep", {"pattern": "TODO|FIXME", "path": "src"})])
result = _hint([_tc("grep", {"pattern": "TODO|FIXME", "path": "src"})])
assert result == 'grep "TODO|FIXME"'
def test_exec_shows_command(self):
result = AgentLoop._tool_hint([_tc("exec", {"command": "npm install typescript"})])
result = _hint([_tc("exec", {"command": "npm install typescript"})])
assert result == "$ npm install typescript"
def test_exec_truncates_long_command(self):
cmd = "cd /very/long/path && cat file && echo done && sleep 1 && ls -la"
result = AgentLoop._tool_hint([_tc("exec", {"command": cmd})])
result = _hint([_tc("exec", {"command": cmd})])
assert result.startswith("$ ")
assert len(result) <= 50 # reasonable limit
def test_web_search(self):
result = AgentLoop._tool_hint([_tc("web_search", {"query": "Claude 4 vs GPT-4"})])
result = _hint([_tc("web_search", {"query": "Claude 4 vs GPT-4"})])
assert result == 'search "Claude 4 vs GPT-4"'
def test_web_fetch(self):
result = AgentLoop._tool_hint([_tc("web_fetch", {"url": "https://example.com/page"})])
result = _hint([_tc("web_fetch", {"url": "https://example.com/page"})])
assert result == "fetch https://example.com/page"
@@ -60,12 +65,12 @@ class TestToolHintMCP:
"""Test MCP tools are abbreviated to server::tool format."""
def test_mcp_standard_format(self):
result = AgentLoop._tool_hint([_tc("mcp_4_5v_mcp__analyze_image", {"imageSource": "https://img.jpg", "prompt": "describe"})])
result = _hint([_tc("mcp_4_5v_mcp__analyze_image", {"imageSource": "https://img.jpg", "prompt": "describe"})])
assert "4_5v" in result
assert "analyze_image" in result
def test_mcp_simple_name(self):
result = AgentLoop._tool_hint([_tc("mcp_github__create_issue", {"title": "Bug fix"})])
result = _hint([_tc("mcp_github__create_issue", {"title": "Bug fix"})])
assert "github" in result
assert "create_issue" in result
@@ -74,21 +79,21 @@ class TestToolHintFallback:
"""Test unknown tools fall back to original behavior."""
def test_unknown_tool_with_string_arg(self):
result = AgentLoop._tool_hint([_tc("custom_tool", {"data": "hello world"})])
result = _hint([_tc("custom_tool", {"data": "hello world"})])
assert result == 'custom_tool("hello world")'
def test_unknown_tool_with_long_arg_truncates(self):
long_val = "a" * 60
result = AgentLoop._tool_hint([_tc("custom_tool", {"data": long_val})])
result = _hint([_tc("custom_tool", {"data": long_val})])
assert len(result) < 80
assert "\u2026" in result
def test_unknown_tool_no_string_arg(self):
result = AgentLoop._tool_hint([_tc("custom_tool", {"count": 42})])
result = _hint([_tc("custom_tool", {"count": 42})])
assert result == "custom_tool"
def test_empty_tool_calls(self):
result = AgentLoop._tool_hint([])
result = _hint([])
assert result == ""
@@ -97,7 +102,7 @@ class TestToolHintFolding:
def test_single_call_no_fold(self):
calls = [_tc("grep", {"pattern": "*.py"})]
result = AgentLoop._tool_hint(calls)
result = _hint(calls)
assert "\u00d7" not in result
def test_two_consecutive_same_folded(self):
@@ -105,7 +110,7 @@ class TestToolHintFolding:
_tc("grep", {"pattern": "*.py"}),
_tc("grep", {"pattern": "*.ts"}),
]
result = AgentLoop._tool_hint(calls)
result = _hint(calls)
assert "\u00d7 2" in result
def test_three_consecutive_same_folded(self):
@@ -114,7 +119,7 @@ class TestToolHintFolding:
_tc("read_file", {"path": "b.py"}),
_tc("read_file", {"path": "c.py"}),
]
result = AgentLoop._tool_hint(calls)
result = _hint(calls)
assert "\u00d7 3" in result
def test_different_tools_not_folded(self):
@@ -122,7 +127,7 @@ class TestToolHintFolding:
_tc("grep", {"pattern": "TODO"}),
_tc("read_file", {"path": "a.py"}),
]
result = AgentLoop._tool_hint(calls)
result = _hint(calls)
assert "\u00d7" not in result
def test_interleaved_same_tools_not_folded(self):
@@ -131,7 +136,7 @@ class TestToolHintFolding:
_tc("read_file", {"path": "f.py"}),
_tc("grep", {"pattern": "b"}),
]
result = AgentLoop._tool_hint(calls)
result = _hint(calls)
assert "\u00d7" not in result
@@ -143,7 +148,7 @@ class TestToolHintMultipleCalls:
_tc("grep", {"pattern": "TODO"}),
_tc("read_file", {"path": "main.py"}),
]
result = AgentLoop._tool_hint(calls)
result = _hint(calls)
assert 'grep "TODO"' in result
assert "read main.py" in result
assert ", " in result
@@ -154,27 +159,27 @@ class TestToolHintEdgeCases:
def test_known_tool_empty_list_args(self):
"""C1/G1: Empty list arguments should not crash."""
result = AgentLoop._tool_hint([_tc("read_file", [])])
result = _hint([_tc("read_file", [])])
assert result == "read_file"
def test_known_tool_none_args(self):
"""G2: None arguments should not crash."""
result = AgentLoop._tool_hint([_tc("read_file", None)])
result = _hint([_tc("read_file", None)])
assert result == "read_file"
def test_fallback_empty_list_args(self):
"""C1: Empty list args in fallback should not crash."""
result = AgentLoop._tool_hint([_tc("custom_tool", [])])
result = _hint([_tc("custom_tool", [])])
assert result == "custom_tool"
def test_fallback_none_args(self):
"""G2: None args in fallback should not crash."""
result = AgentLoop._tool_hint([_tc("custom_tool", None)])
result = _hint([_tc("custom_tool", None)])
assert result == "custom_tool"
def test_list_dir_registered(self):
"""S2: list_dir should use 'ls' format."""
result = AgentLoop._tool_hint([_tc("list_dir", {"path": "/tmp"})])
result = _hint([_tc("list_dir", {"path": "/tmp"})])
assert result == "ls /tmp"
@@ -190,7 +195,7 @@ class TestToolHintMixedFolding:
_tc("grep", {"pattern": "y"}),
_tc("read_file", {"path": "c.py"}),
]
result = AgentLoop._tool_hint(calls)
result = _hint(calls)
assert "\u00d7 2" in result
# Should have 3 groups: read×2, grep×2, read
parts = result.split(", ")