"""Tool registration and the @tool decorator.""" from __future__ import annotations import asyncio import inspect from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any from aicc.protocol import ToolClass from aicc.schema import function_schema, return_schema ToolImpl = Callable[..., Any | Awaitable[Any]] @dataclass class ToolDefinition: id: str cls: ToolClass description: str fn: ToolImpl input_schema: dict[str, Any] output_schema: dict[str, Any] requires_capability: str | None = None limits: dict[str, Any] | None = None strict_input: bool = False def tool( _fn: ToolImpl | None = None, *, id: str | None = None, cls: ToolClass | None = None, description: str | None = None, requires_capability: str | None = None, limits: dict[str, Any] | None = None, strict_input: bool = False, ) -> Any: """Mark a function as an AICC tool. Use as @tool or @tool(cls=ToolClass.SENSOR, description=...). """ def wrap(fn: ToolImpl) -> ToolDefinition: tool_id = id or fn.__name__ if not tool_id.replace("_", "").isalnum() or tool_id[0].isdigit(): raise ValueError( f"Tool id '{tool_id}' must match ^[a-z][a-z0-9_]*$ " "(lowercase letters, digits, underscores; cannot start with digit)." ) tool_cls = cls or _infer_class(fn) if description: desc = description else: doc = inspect.getdoc(fn) or "" desc = doc.strip().splitlines()[0] if doc else "" if not desc: raise ValueError( f"Tool '{tool_id}' requires a description (set description= or add a docstring)." ) return ToolDefinition( id=tool_id, cls=tool_cls, description=desc, fn=fn, input_schema=function_schema(fn), output_schema=return_schema(fn), requires_capability=requires_capability, limits=limits, strict_input=strict_input, ) if _fn is not None and callable(_fn): return wrap(_fn) return wrap def _infer_class(fn: ToolImpl) -> ToolClass: """Default to sensor if the tool starts with get_/read_/observe_; otherwise actuator.""" name = fn.__name__.lower() if any(name.startswith(p) for p in ("get_", "read_", "observe_", "inspect_", "see_", "hear_", "smell_", "touch_")): return ToolClass.SENSOR return ToolClass.ACTUATOR async def call_tool_impl(defn: ToolDefinition, input_data: dict[str, Any]) -> Any: """Invoke a registered tool's implementation with the given input.""" if defn.strict_input: from jsonschema import validate as _validate # type: ignore _validate(instance=input_data, schema=defn.input_schema) result = defn.fn(**input_data) if input_data else defn.fn() if asyncio.iscoroutine(result) or inspect.isawaitable(result): result = await result # type: ignore[func-returns-value] return result