Spaces:
Runtime error
Runtime error
| """ | |
| toolset.py — the @tool decorator that toolset code files import. | |
| Agent-centric alternative to skills. One doc + one code file. The agent | |
| calls tool_store(action="execute", tool_name="...") and it just runs. | |
| Usage inside a toolset code file:: | |
| from toolstore.toolset import tool | |
| @tool | |
| def get_weather(*, location: str, units: str = "metric") -> dict: | |
| '''Get current weather for a location. | |
| Args: | |
| location: City name or ZIP code. | |
| units: "metric" or "imperial". | |
| ''' | |
| import httpx | |
| ... | |
| Only functions decorated with @tool are callable by agents. Everything | |
| else in the module is private helper code. | |
| The decorator also enables schema auto‑generation — publishing to the | |
| ToolStore registry derives the OpenAI function‑calling JSON from | |
| type hints and docstrings, no manual JSON needed. | |
| """ | |
| from __future__ import annotations | |
| import inspect | |
| import json | |
| import re | |
| from pathlib import Path | |
| from typing import Any, Callable, get_type_hints | |
| # Per‑module registry — cleared before each toolset load. | |
| _REGISTRY: dict[str, Callable[..., Any]] = {} | |
| # ── Python type → JSON Schema type ──────────────────────────────────── | |
| _TYPE_MAP: dict[type, str] = { | |
| str: "string", | |
| int: "integer", | |
| float: "number", | |
| bool: "boolean", | |
| list: "array", | |
| dict: "object", | |
| type(None): "null", | |
| } | |
| def _py_to_json_type(py_type: type) -> str: | |
| """Map a Python type to a JSON Schema type string.""" | |
| origin = getattr(py_type, "__origin__", None) | |
| if origin is not None: | |
| # typing.List[X], typing.Optional[X], etc. | |
| args = getattr(py_type, "__args__", ()) | |
| if origin is list: | |
| return "array" | |
| if origin is dict: | |
| return "object" | |
| # Optional[X] → Union[X, None] → use the non‑None arg | |
| if origin is type(None).__class__.__base__ or str(origin).endswith("Union"): | |
| non_none = [a for a in args if a is not type(None)] | |
| if non_none: | |
| return _py_to_json_type(non_none[0]) | |
| return "string" | |
| return _TYPE_MAP.get(py_type, "string") | |
| def tool(fn: Callable[..., Any]) -> Callable[..., Any]: | |
| """Decorator: mark a function as an agent‑callable toolset entry point. | |
| The function name becomes the binding name. The docstring becomes the | |
| binding description. The signature (type hints + defaults) defines the | |
| input schema that the agent sees. | |
| """ | |
| _REGISTRY[fn.__name__] = fn | |
| fn._is_toolset_tool = True # type: ignore[attr-defined] | |
| return fn | |
| def get_tool(name: str) -> Callable[..., Any] | None: | |
| """Return a registered @tool function by name, or None.""" | |
| return _REGISTRY.get(name) | |
| def get_tool_names() -> list[str]: | |
| """Return the names of all registered @tool functions.""" | |
| return list(_REGISTRY.keys()) | |
| def clear_registry() -> None: | |
| """Clear the registry (called between loading different toolsets).""" | |
| _REGISTRY.clear() | |
| # ── Schema auto‑generation ───────────────────────────────────────────── | |
| def generate_definition(fn: Callable[..., Any]) -> dict: | |
| """Generate an OpenAI function‑calling definition from a @tool function. | |
| Reads: | |
| • fn.__name__ → definition.name | |
| • fn.__doc__ → definition.description | |
| • fn.__annotations__ → definition.parameters.properties | |
| • docstring ``Args:`` section → per‑parameter descriptions | |
| • default values → parameters.required (excludes those with defaults) | |
| Returns a dict ready for the ToolStore publish endpoint's ``definition`` | |
| field. | |
| """ | |
| hints = _safe_get_type_hints(fn) | |
| sig = inspect.signature(fn) | |
| # Docstring: first paragraph = description | |
| doc = inspect.getdoc(fn) or "" | |
| desc_parts = doc.split("\n\n") | |
| description = desc_parts[0].strip() if desc_parts else fn.__name__ | |
| # Parse ``Args:`` block from docstring for per‑param descriptions | |
| param_descriptions = _parse_docstring_args(doc) | |
| if not param_descriptions: | |
| param_descriptions = _parse_google_args(doc) | |
| properties = {} | |
| required: list[str] = [] | |
| for param_name, param in sig.parameters.items(): | |
| if param_name in ("self", "cls"): | |
| continue | |
| if param_name.startswith("_"): | |
| continue | |
| py_type = hints.get(param_name, str) | |
| json_type = _py_to_json_type(py_type) | |
| prop: dict[str, Any] = {"type": json_type} | |
| description_text = param_descriptions.get(param_name, "") | |
| if description_text: | |
| prop["description"] = description_text | |
| # Handle Optional — mark nullable | |
| if _is_optional(py_type): | |
| prop["nullable"] = True | |
| properties[param_name] = prop | |
| # Required = no default value AND not Optional | |
| if param.default is inspect.Parameter.empty and not _is_optional(py_type): | |
| required.append(param_name) | |
| return { | |
| "name": fn.__name__, | |
| "description": description, | |
| "parameters": { | |
| "type": "object", | |
| "properties": properties, | |
| "required": required, | |
| }, | |
| } | |
| def _safe_get_type_hints(fn: Callable[..., Any]) -> dict[str, type]: | |
| """Get type hints, swallowing any errors from forward refs.""" | |
| try: | |
| return get_type_hints(fn) | |
| except Exception: | |
| return fn.__annotations__ or {} | |
| def _parse_docstring_args(doc: str) -> dict[str, str]: | |
| """Parse Sphinx‑style ``Args:`` section from docstring. | |
| Matches:: | |
| Args: | |
| param_name: Description text. | |
| param_name2: More text that may | |
| span multiple lines. | |
| """ | |
| result: dict[str, str] = {} | |
| lines = doc.split("\n") | |
| in_args = False | |
| current_param: str | None = None | |
| current_desc: list[str] = [] | |
| for line in lines: | |
| stripped = line.strip() | |
| if re.match(r"^Args\s*:", stripped): | |
| in_args = True | |
| continue | |
| if in_args and re.match(r"^(Returns|Raises|Yields|Notes|Examples)\s*:", stripped): | |
| break | |
| if in_args and stripped == "": | |
| if current_param: | |
| result[current_param] = " ".join(current_desc).strip() | |
| current_param = None | |
| current_desc = [] | |
| continue | |
| if in_args: | |
| # Check for "param_name:" pattern (not indented continuation) | |
| m = re.match(r"^(\w[\w\d_]*)\s*:\s*(.*)", stripped) | |
| if m: | |
| if current_param: | |
| result[current_param] = " ".join(current_desc).strip() | |
| current_param = m.group(1) | |
| current_desc = [m.group(2).strip()] if m.group(2).strip() else [] | |
| elif current_param and stripped: | |
| # Continuation of previous param description | |
| current_desc.append(stripped) | |
| if current_param: | |
| result[current_param] = " ".join(current_desc).strip() | |
| return result | |
| def _parse_google_args(doc: str) -> dict[str, str]: | |
| """Fallback: parse Google‑style ``Args:`` where params are indented.:: | |
| Args: | |
| param_name (type): Description. | |
| """ | |
| result: dict[str, str] = {} | |
| pattern = re.compile(r"^\s*(\w[\w\d_]*)\s*(?:\([^)]*\))?\s*:\s*(.*)") | |
| lines = doc.split("\n") | |
| in_args = False | |
| for line in lines: | |
| if re.match(r"^\s*Args\s*:", line): | |
| in_args = True | |
| continue | |
| if in_args and re.match(r"^\s*(Returns|Raises|Yields)\s*:", line): | |
| break | |
| if in_args: | |
| m = pattern.match(line) | |
| if m: | |
| result[m.group(1)] = m.group(2).strip() | |
| return result | |
| def _is_optional(py_type: type) -> bool: | |
| """Return True if the type is Optional[X] (Union[X, None]).""" | |
| origin = getattr(py_type, "__origin__", None) | |
| if origin is None: | |
| return False | |
| # Check if it's a Union containing NoneType | |
| args = getattr(py_type, "__args__", ()) | |
| if len(args) == 2 and type(None) in args: | |
| return True | |
| return False | |