Newer
Older
navi-1 / navi / tools / test_tool.py
"""Test tool — run a user tool's execute() in isolation and return the result or traceback."""

import importlib
import importlib.util
import sys
import traceback
from pathlib import Path

from navi.config import settings

from ._internal.base import Tool, ToolResult


class TestToolTool(Tool):
    name = "test_tool"
    description = (
        "Run a user tool's execute() function with given params and return the result or full traceback. "
        "Always use this after writing or editing a tool file to verify it works before calling reload_tools."
    )
    parameters = {
        "type": "object",
        "properties": {
            "tool_name": {
                "type": "string",
                "description": "Name of the tool to test (filename without .py, e.g. 'my_tool').",
            },
            "params": {
                "type": "object",
                "description": "Parameters dict to pass to execute(). Omit or pass {} for tools with no required params.",
            },
        },
        "required": ["tool_name"],
    }

    async def execute(self, params: dict) -> ToolResult:
        tool_name = (params.get("tool_name") or "").strip()
        test_params: dict = params.get("params") or {}

        if not tool_name:
            return ToolResult(success=False, output="tool_name is required.", error="missing tool_name")

        tool_path = Path(settings.tools_dir) / f"{tool_name}.py"
        if not tool_path.exists():
            return ToolResult(
                success=False,
                output=f"File not found: {tool_path}",
                error="file not found",
            )

        # Force a fresh import from disk — bypasses any cached (possibly stale) module
        module_key = f"_test_tool_run_{tool_name}"
        spec = importlib.util.spec_from_file_location(module_key, tool_path)
        if spec is None or spec.loader is None:
            return ToolResult(success=False, output=f"Cannot load spec for {tool_path}", error="spec error")

        module = importlib.util.module_from_spec(spec)
        try:
            spec.loader.exec_module(module)  # type: ignore[union-attr]
        except Exception:
            tb = traceback.format_exc()
            return ToolResult(success=False, output=f"Module load error:\n{tb}", error="load error")

        execute_fn = getattr(module, "execute", None)
        if execute_fn is None:
            return ToolResult(success=False, output="Module has no execute() function.", error="missing execute")

        try:
            result = await execute_fn(test_params)
        except Exception:
            tb = traceback.format_exc()
            return ToolResult(success=False, output=f"execute() raised an exception:\n{tb}", error="runtime error")

        return ToolResult(success=True, output=f"OK\n\nResult: {result}")