"""The `skills` tool — find and load skills and protocols from the catalog."""
from __future__ import annotations
import structlog
from navi.skills import SkillError, SkillRef, SkillStore
from navi.tools._internal.base import (
Tool,
ToolContext,
ToolResult,
current_user_id,
)
log = structlog.get_logger()
_KIND_HINT = "'skill' — a flexible approach with heuristics; 'protocol' — a strict ordered procedure."
class SkillTool(Tool):
name = "skills"
description = (
"The catalog of skills and protocols — reusable methodologies this Navi has been "
"taught, owned by you or shared by the hive.\n\n"
"Call 'search' before improvising an approach to a recurring kind of task (home "
f"automation, shutting down or bringing up infrastructure, workflows). {_KIND_HINT}\n\n"
"Call 'load' with the id from a search result (or an exact name) to get the full "
"instructions. A skill is advice: follow it as far as it fits. A protocol is a "
"commitment: its steps are run in order with a check after each, and its escape "
"hatches must be honoured when reality differs from the plan.\n\n"
"Search costs one call and often replaces several turns of rediscovery — use it."
)
parameters = {
"type": "object",
"properties": {
"op": {
"type": "string",
"enum": ["search", "load"],
"description": "search — find entries; load — read one entry in full.",
},
"query": {
"type": "string",
"description": (
"search: words to match against name, description, triggers and tags "
"(e.g. 'energy saving', 'shutdown', 'backup')."
),
},
"kind": {
"type": "string",
"enum": ["skill", "protocol"],
"description": "Only entries of this kind.",
},
"tag": {"type": "string", "description": "search: require this tag."},
"area": {
"type": "string",
"enum": ["home", "infrastructure", "workflows", "general"],
"description": "search: only this area.",
},
"id": {"type": "string", "description": "load: the entry id from search."},
"name": {
"type": "string",
"description": "load: the exact name, when you know it and have no id.",
},
},
"required": ["op"],
}
def __init__(self, store: SkillStore) -> None:
self._store = store
async def execute(self, params: dict, ctx: ToolContext | None = None) -> ToolResult:
op = params.get("op")
user_id = current_user_id.get(None) or (ctx.user_id if ctx else None) or ""
if op == "search":
try:
entries = await self._store.list(
user_id=user_id,
kind=params.get("kind"),
query=(params.get("query") or "").strip() or None,
tag=(params.get("tag") or "").strip() or None,
area=params.get("area"),
)
except Exception as e: # the catalog being down must not look like "nothing found"
log.warning("skills.search_failed", error=str(e))
return ToolResult(success=False, output="", error=f"Cannot search the catalog: {e}")
if not entries:
return ToolResult(
success=True,
output=(
"No matching skills or protocols. Nothing is stored for this query yet — "
"either it was never taught, or the wording differs: try one word "
"('power', 'backup', 'shutdown') or drop the filters."
),
)
lines = [f"{len(entries)} matching entries (personal first):"]
lines.extend(SkillRef.of(e).line() for e in entries)
lines.append("Load one with op='load' and its id.")
return ToolResult(success=True, output="\n".join(lines))
if op == "load":
entry_id = (params.get("id") or "").strip()
name = (params.get("name") or "").strip()
if not entry_id and not name:
return ToolResult(
success=False, output="",
error="'id' or 'name' is required for 'load'",
)
try:
entry = await self._store.find(
user_id=user_id, skill_id=entry_id or None,
name=name or None, kind=params.get("kind"),
)
except Exception as e:
log.warning("skills.load_failed", error=str(e))
return ToolResult(success=False, output="", error=f"Cannot read the catalog: {e}")
if entry is None:
return ToolResult(
success=False, output="",
error=(
f"No such entry ({entry_id or name!r}). Search first: "
"op='search' with a word from its name or purpose."
),
)
try:
rendered = entry.doc
except SkillError as e:
return ToolResult(
success=False, output="",
error=f"The stored document is invalid and cannot be loaded: {e}",
)
from navi.skills import render_for_agent
return ToolResult(
success=True,
output=render_for_agent(
rendered, entry_id=entry.id, version=entry.version,
visibility=entry.visibility,
),
metadata={
"skill_id": entry.id,
"kind": entry.kind,
"name": entry.name,
"version": entry.version,
},
)
return ToolResult(
success=False, output="",
error=f"Unknown op: {op!r}. Use 'search' or 'load'.",
)