mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
The llm tool called upstream providers through the litellm fork, read provider keys, and never reached the MCP surface: its register() was a no-op and it skipped BaseTool's constructor. Kai had no Python tool. hanzo-tools-llm now carries two HIP-0300 tools over HanzoCloud: - llm (default action query): POST /v1/chat/completions with model enso-auto unless named, "auto" for Enso's pick, max_cost and max_latency_ms as X-Max-Cost and X-Max-Latency-Ms, returning id and the model that served; models: GET /v1/models, one row per model with the catalog's per-million prices, narrowed by family; feedback: POST /v1/ai/feedback with request_id, signal and a 1-3 rating. - kai_decide (default action decide): POST /v1/decisions with the TypeScript tool's schema and shape checks. The consensus, llm_manage and provider modules wrapped the upstream client and were never registered, so they go, with hanzo-consensus and the [full] extra. hanzo-tools-llm 0.2.1. Co-authored-by: Hanzo Dev <[email protected]>
307 lines
9.3 KiB
Python
307 lines
9.3 KiB
Python
"""Test all hanzo-tools-* packages import and register correctly."""
|
|
|
|
import sys
|
|
import time
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
|
|
def _module_installed(module_name: str) -> bool:
|
|
"""Check if a module is installed."""
|
|
try:
|
|
__import__(module_name)
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
|
|
class TestToolPackages:
|
|
"""Test that all tool packages import correctly."""
|
|
|
|
def test_core_imports(self):
|
|
"""Test hanzo-tools-core imports."""
|
|
from hanzo_tools.core import BaseTool, ToolContext
|
|
from hanzo_tools.core.decorators import auto_timeout
|
|
from hanzo_tools.core.permissions import PermissionManager
|
|
|
|
assert BaseTool is not None
|
|
assert ToolContext is not None
|
|
assert PermissionManager is not None
|
|
assert auto_timeout is not None
|
|
|
|
def test_fs_tools(self):
|
|
"""Test hanzo-tools-fs exports unified HIP-0300 tool."""
|
|
from hanzo_tools.fs import TOOLS
|
|
|
|
assert len(TOOLS) == 1
|
|
|
|
def test_shell_tools(self):
|
|
"""Test hanzo-tools-shell exports detected shell + core shell tools."""
|
|
from hanzo_tools.shell import TOOLS
|
|
|
|
# First tool is detected shell (environment-dependent), then fixed base tools.
|
|
assert len(TOOLS) >= 8
|
|
names = {t.name for t in TOOLS}
|
|
required = {"ps", "npx", "uvx", "open", "curl", "jq", "wget"}
|
|
assert required.issubset(names)
|
|
|
|
def test_memory_tools(self):
|
|
"""Test hanzo-tools-memory exports unified memory tool."""
|
|
from hanzo_tools.memory import TOOLS
|
|
|
|
assert len(TOOLS) == 1
|
|
|
|
def test_todo_tools(self):
|
|
"""Test hanzo-tools-todo has 1 tool."""
|
|
from hanzo_tools.todo import TOOLS
|
|
|
|
assert len(TOOLS) == 1
|
|
assert TOOLS[0].name == "tasks"
|
|
|
|
def test_reasoning_tools(self):
|
|
"""Test hanzo-tools-reasoning has 2 tools."""
|
|
from hanzo_tools.reasoning import TOOLS
|
|
|
|
assert len(TOOLS) == 2
|
|
names = {t.name for t in TOOLS}
|
|
assert names == {"think", "critic"}
|
|
|
|
def test_lsp_tools(self):
|
|
"""Test hanzo-tools-lsp has 1 tool."""
|
|
from hanzo_tools.lsp import TOOLS
|
|
|
|
assert len(TOOLS) == 1
|
|
assert TOOLS[0].name == "lsp"
|
|
|
|
def test_refactor_tools(self):
|
|
"""Test hanzo-tools-refactor has 1 tool."""
|
|
from hanzo_tools.refactor import TOOLS
|
|
|
|
assert len(TOOLS) == 1
|
|
assert TOOLS[0].name == "refactor"
|
|
|
|
@pytest.mark.skipif(
|
|
not _module_installed("hanzo_tools.database"),
|
|
reason="hanzo-tools-database not installed",
|
|
)
|
|
def test_database_tools(self):
|
|
"""Test hanzo-tools-database has 9 tools."""
|
|
from hanzo_tools.database import TOOLS
|
|
|
|
assert len(TOOLS) == 9
|
|
|
|
def test_agent_tools(self):
|
|
"""Test hanzo-tools-agent has 3 tools.
|
|
|
|
Core tools: AgentTool, ZenTool, ReviewTool
|
|
"""
|
|
from hanzo_tools.agent import TOOLS
|
|
|
|
assert len(TOOLS) == 3, f"Expected 3 agent tools, got {len(TOOLS)}"
|
|
names = {t.name for t in TOOLS}
|
|
assert names == {"agent", "zen", "review"}
|
|
|
|
def test_jupyter_tools(self):
|
|
"""Test hanzo-tools-jupyter has 1 tool."""
|
|
from hanzo_tools.jupyter import TOOLS
|
|
|
|
assert len(TOOLS) == 1
|
|
|
|
@pytest.mark.skipif(
|
|
not _module_installed("hanzo_tools.editor"),
|
|
reason="hanzo-tools-editor not installed",
|
|
)
|
|
def test_editor_tools(self):
|
|
"""Test hanzo-tools-editor has 3 tools."""
|
|
from hanzo_tools.editor import TOOLS
|
|
|
|
assert len(TOOLS) == 3
|
|
|
|
def test_browser_tools(self):
|
|
"""Test hanzo-tools-browser has 3 tools (browser, cdp, playwright)."""
|
|
from hanzo_tools.browser import TOOLS
|
|
|
|
assert len(TOOLS) == 3
|
|
|
|
@pytest.mark.skipif(
|
|
not _module_installed("hanzo_tools.config"),
|
|
reason="hanzo-tools-config not installed",
|
|
)
|
|
def test_config_tools(self):
|
|
"""Test hanzo-tools-config has at least 1 tool (graceful degradation)."""
|
|
from hanzo_tools.config import TOOLS
|
|
|
|
assert len(TOOLS) >= 1
|
|
|
|
@pytest.mark.skipif(
|
|
not _module_installed("hanzo_tools.mcp_tools"),
|
|
reason="hanzo-tools-mcp not installed",
|
|
)
|
|
def test_mcp_tools(self):
|
|
"""Test hanzo-tools-mcp has 5 tools."""
|
|
from hanzo_tools.mcp_tools import TOOLS
|
|
|
|
assert len(TOOLS) == 5
|
|
|
|
@pytest.mark.skipif(
|
|
not _module_installed("hanzo_tools.llm"),
|
|
reason="hanzo-tools-llm not installed",
|
|
)
|
|
def test_llm_tools(self):
|
|
"""Test hanzo-tools-llm carries llm and kai_decide, both over HanzoCloud."""
|
|
from hanzo_tools.llm import TOOLS
|
|
|
|
assert [t.name for t in TOOLS] == ["llm", "kai_decide"]
|
|
|
|
@pytest.mark.skipif(
|
|
not _module_installed("hanzo_tools.vector"),
|
|
reason="hanzo-tools-vector not installed",
|
|
)
|
|
def test_vector_tools(self):
|
|
"""Test hanzo-tools-vector imports (tools depend on heavy deps)."""
|
|
from hanzo_tools.vector import TOOLS, VECTOR_AVAILABLE
|
|
|
|
# Vector tools are optional, depend on faiss/qdrant
|
|
if VECTOR_AVAILABLE:
|
|
assert len(TOOLS) >= 1
|
|
else:
|
|
assert len(TOOLS) == 0
|
|
|
|
|
|
# Required packages for import speed tests
|
|
REQUIRED_IMPORT_MODULES = [
|
|
("hanzo_tools.core", 1.0),
|
|
("hanzo_tools.fs", 1.0),
|
|
("hanzo_tools.shell", 1.0),
|
|
("hanzo_tools.memory", 1.0),
|
|
("hanzo_tools.todo", 1.0),
|
|
("hanzo_tools.reasoning", 1.0),
|
|
("hanzo_tools.lsp", 1.0),
|
|
("hanzo_tools.refactor", 1.0),
|
|
("hanzo_tools.jupyter", 1.0),
|
|
("hanzo_tools.browser", 1.0),
|
|
("hanzo_tools.agent", 2.0), # Agent has llm, allow more time
|
|
]
|
|
|
|
# Optional packages that may not be installed
|
|
OPTIONAL_IMPORT_MODULES = [
|
|
("hanzo_tools.config", 1.0),
|
|
("hanzo_tools.mcp_tools", 1.0),
|
|
("hanzo_tools.database", 1.0),
|
|
("hanzo_tools.editor", 1.0),
|
|
("hanzo_tools.llm", 1.0),
|
|
("hanzo_tools.vector", 2.0), # Vector has heavy deps
|
|
]
|
|
|
|
|
|
class TestToolImportSpeed:
|
|
"""Test that tool imports are fast (no blocking)."""
|
|
|
|
@pytest.mark.parametrize("module,max_time", REQUIRED_IMPORT_MODULES)
|
|
def test_import_speed_required(self, module, max_time):
|
|
"""Test that required imports complete quickly."""
|
|
import importlib
|
|
|
|
start = time.time()
|
|
importlib.import_module(module)
|
|
elapsed = time.time() - start
|
|
assert elapsed < max_time, f"{module} took {elapsed:.2f}s (max {max_time}s)"
|
|
|
|
@pytest.mark.parametrize("module,max_time", OPTIONAL_IMPORT_MODULES)
|
|
def test_import_speed_optional(self, module, max_time):
|
|
"""Test that optional imports complete quickly (if installed)."""
|
|
if not _module_installed(module):
|
|
pytest.skip(f"{module} not installed")
|
|
import importlib
|
|
|
|
start = time.time()
|
|
importlib.import_module(module)
|
|
elapsed = time.time() - start
|
|
assert elapsed < max_time, f"{module} took {elapsed:.2f}s (max {max_time}s)"
|
|
|
|
|
|
# Packages that must always be testable
|
|
REQUIRED_PACKAGES = [
|
|
"hanzo_tools.fs",
|
|
"hanzo_tools.shell",
|
|
"hanzo_tools.memory",
|
|
"hanzo_tools.todo",
|
|
"hanzo_tools.reasoning",
|
|
"hanzo_tools.lsp",
|
|
"hanzo_tools.refactor",
|
|
"hanzo_tools.agent",
|
|
"hanzo_tools.jupyter",
|
|
"hanzo_tools.browser",
|
|
]
|
|
|
|
# Optional packages
|
|
OPTIONAL_PACKAGES = [
|
|
"hanzo_tools.config",
|
|
"hanzo_tools.mcp_tools",
|
|
"hanzo_tools.database",
|
|
"hanzo_tools.editor",
|
|
"hanzo_tools.llm",
|
|
"hanzo_tools.vector",
|
|
]
|
|
|
|
|
|
class TestToolAsync:
|
|
"""Test that all tools have async call methods."""
|
|
|
|
def test_all_tools_async(self):
|
|
"""Verify all tool .call() methods are async."""
|
|
import importlib
|
|
|
|
all_packages = REQUIRED_PACKAGES + [
|
|
p for p in OPTIONAL_PACKAGES if _module_installed(p)
|
|
]
|
|
|
|
for pkg_name in all_packages:
|
|
pkg = importlib.import_module(pkg_name)
|
|
tools = getattr(pkg, "TOOLS", [])
|
|
for tool in tools:
|
|
if hasattr(tool, "call"):
|
|
assert asyncio.iscoroutinefunction(
|
|
tool.call
|
|
), f"{pkg_name}.{tool.name}.call() is not async"
|
|
|
|
|
|
class TestTotalToolCount:
|
|
"""Test total tool count across all packages."""
|
|
|
|
def test_required_tool_count(self):
|
|
"""Verify we have the expected tools in required packages.
|
|
|
|
Required packages must have exact counts.
|
|
"""
|
|
# Required packages with exact counts
|
|
required_packages = [
|
|
("hanzo_tools.fs", 1),
|
|
("hanzo_tools.shell", 9),
|
|
("hanzo_tools.browser", 3),
|
|
("hanzo_tools.memory", 1), # unified memory tool
|
|
("hanzo_tools.todo", 1),
|
|
("hanzo_tools.reasoning", 2),
|
|
("hanzo_tools.lsp", 1),
|
|
("hanzo_tools.refactor", 1),
|
|
("hanzo_tools.jupyter", 1),
|
|
("hanzo_tools.agent", 3),
|
|
]
|
|
|
|
total = 0
|
|
import importlib
|
|
|
|
# Check required packages (exact counts)
|
|
for pkg_name, expected_count in required_packages:
|
|
pkg = importlib.import_module(pkg_name)
|
|
tools = getattr(pkg, "TOOLS", [])
|
|
actual = len(tools)
|
|
assert (
|
|
actual == expected_count
|
|
), f"{pkg_name}: expected {expected_count} tools, got {actual}"
|
|
total += actual
|
|
|
|
# Required tools: 23 (1+9+3+1+1+2+1+1+1+3)
|
|
assert total == 23, f"Expected 23 required tools, got {total}"
|