Newer
Older
navi-1 / navi / tools / tool_manual.py
"""Built-in tool that returns the detailed manual for a given tool."""

from pathlib import Path

from ._internal.base import Tool, ToolResult

MANUALS_DIR = Path(__file__).parent.parent.parent / "manuals"


class ToolManualTool(Tool):
    name = "tool_manual"
    description = (
        "Returns the detailed manual for a tool: full usage instructions, parameter reference, and examples. "
        "Call this before using an unfamiliar tool, or when you are unsure about the correct format or parameters."
    )
    parameters = {
        "type": "object",
        "properties": {
            "tool_name": {
                "type": "string",
                "description": "Name of the tool to look up, e.g. 'create_mcp_server'",
            }
        },
        "required": ["tool_name"],
    }

    def __init__(self, registry=None) -> None:
        self._registry = registry

    async def execute(self, params: dict) -> ToolResult:
        tool_name = params["tool_name"].strip()

        manual_file = MANUALS_DIR / f"{tool_name}.md"
        if manual_file.exists():
            return ToolResult(success=True, output=manual_file.read_text(encoding="utf-8"))

        # No .md file — generate a manual from the tool's schema
        if self._registry:
            try:
                tool = self._registry.get(tool_name)
                return ToolResult(success=True, output=_auto_manual(tool))
            except Exception:
                pass

        available = sorted(f.stem for f in MANUALS_DIR.glob("*.md")) if MANUALS_DIR.exists() else []
        hint = f"\nAvailable manuals: {', '.join(available)}" if available else ""
        return ToolResult(success=False, output=f"No manual found for '{tool_name}'.{hint}", error="not_found")


def _auto_manual(tool) -> str:
    """Generate a readable manual from a tool's schema."""
    lines = [f"# {tool.name}", "", tool.description, "", "## Parameters"]

    props = tool.parameters.get("properties", {})
    required = set(tool.parameters.get("required", []))

    if not props:
        lines.append("This tool takes no parameters.")
    else:
        for param_name, spec in props.items():
            req = " (required)" if param_name in required else " (optional)"
            ptype = spec.get("type", "any")
            desc = spec.get("description", "")
            enum = spec.get("enum")
            line = f"- `{param_name}` ({ptype}{req}): {desc}"
            if enum:
                line += f" — one of: {', '.join(repr(e) for e in enum)}"
            lines.append(line)

            # Nested object properties
            nested = spec.get("properties", {})
            for nname, nspec in nested.items():
                nreq = " (required)" if nname in spec.get("required", []) else " (optional)"
                ndesc = nspec.get("description", "")
                lines.append(f"  - `{nname}` ({nspec.get('type', 'any')}{nreq}): {ndesc}")

    return "\n".join(lines)