Skip to content

Latest commit

 

History

History
415 lines (327 loc) · 13.4 KB

File metadata and controls

415 lines (327 loc) · 13.4 KB

Tool Maker

Tool Maker lets the AI create custom tools at runtime. The AI writes a Python tool file, validates it, saves it as a plugin, and loads it live — no restart needed.

This is the guide for AI-created tools — simple tool plugins. For full plugin development (hooks, voice commands, schedules, web UIs), see the Plugin Author Guide.


How It Works

  1. AI calls tool_save(name, code) — validates and saves as user/plugins/{name}/
  2. AI calls tool_load() — discovers and activates the new plugin live
  3. The tool appears in the current toolset immediately — no restart

Tool Maker auto-generates a plugin.json manifest from the code. The AI only writes the Python file.


Simple Tool Format

The minimum needed to create a working tool. This is what tool_save expects.

ENABLED = True
AVAILABLE_FUNCTIONS = ['my_func']

TOOLS = [
    {
        "type": "function",
        "is_local": True,
        "function": {
            "name": "my_func",
            "description": "What this tool does and when to use it.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

def execute(function_name, arguments, config, plugin_settings=None):
    if function_name == 'my_func':
        query = arguments.get('query', '')
        return f"Result: {query}", True
    return f"Unknown function: {function_name}", False

Required Exports

Export Type Purpose
ENABLED bool Must be True
AVAILABLE_FUNCTIONS list[str] Function names this file provides
TOOLS list[dict] OpenAI-compatible function schemas
execute() function Dispatches calls, returns (str, bool)

execute() Contract

def execute(function_name, arguments, config, plugin_settings=None):
    """
    Args:
        function_name: Which tool was called (matches TOOLS[].function.name)
        arguments: Dict of parameters from the AI
        config: Sapphire config module (system settings)
        plugin_settings: Dict of this plugin's settings from the Settings UI (or None)

    Returns:
        (result_string, success_bool) — AI sees the result string
    """

Return values:

  • return "Success message", True — worked
  • return "Error: something broke", False — failed, AI sees the error
  • return "No results found", True — empty result (not an error)

Tool Description Tips

The description field in TOOLS is how the AI decides when to call the tool. Make it clear.

# Good — tells AI when to use it
"description": "Convert between temperature units. Use when asked about Fahrenheit/Celsius."

# Bad — doesn't help AI decide
"description": "Temperature converter"

Parameter Types

Standard JSON Schema types: string, integer, number, boolean, array, object.

No parameters:

"parameters": {"type": "object", "properties": {}, "required": []}

Tool with Settings

Tools can declare settings that appear in the web UI Settings page. Users configure them in the browser, the tool reads them at runtime.

ENABLED = True
AVAILABLE_FUNCTIONS = ['weather_get']

TOOLS = [
    {
        "type": "function",
        "is_local": False,
        "function": {
            "name": "weather_get",
            "description": "Get current weather for a city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"}
                },
                "required": ["city"]
            }
        }
    }
]

# Settings — auto-converted to UI fields in the Settings page.
# Types inferred from defaults: str→text input, int/float→number, bool→toggle.
SETTINGS = {
    'WEATHER_API_KEY': '',
    'WEATHER_UNITS': 'metric',
    'WEATHER_CACHE_MIN': 15,
}
SETTINGS_HELP = {
    'WEATHER_API_KEY': 'API key from openweathermap.org',
    'WEATHER_UNITS': 'metric or imperial',
    'WEATHER_CACHE_MIN': 'Cache duration in minutes',
}

def execute(function_name, arguments, config, plugin_settings=None):
    if function_name == 'weather_get':
        settings = plugin_settings or {}
        api_key = settings.get('WEATHER_API_KEY', '')
        if not api_key:
            return "Weather API key not configured. Set it in Settings > Weather.", False

        city = arguments.get('city', '')
        units = settings.get('WEATHER_UNITS', 'metric')

        import requests
        resp = requests.get(
            f"https://api.openweathermap.org/data/2.5/weather",
            params={"q": city, "appid": api_key, "units": units},
            timeout=10
        )
        if resp.status_code != 200:
            return f"Weather API error: {resp.status_code}", False

        data = resp.json()
        temp = data["main"]["temp"]
        desc = data["weather"][0]["description"]
        unit = "°C" if units == "metric" else "°F"
        return f"{city}: {temp}{unit}, {desc}", True

    return f"Unknown function: {function_name}", False

How Settings Work

  1. SETTINGS dict in your code → Tool Maker auto-converts to manifest capabilities.settings
  2. Settings appear in web UI under the plugin's settings tab (auto-rendered, no JavaScript needed)
  3. Read at runtime via the plugin_settings parameter in execute() — no imports needed
  4. SETTINGS_HELP dict (optional) adds descriptions below each field

Type inference from defaults:

  • str → text input
  • int or float → number spinner
  • bool → toggle switch

Settings Key Naming

Prefix SETTINGS keys with your plugin name for clarity and to avoid confusion if multiple plugins have similar settings.

# OK but unclear which plugin owns it
SETTINGS = {'API_KEY': '', 'ZIP_CODE': '10001'}

# Better — clear ownership
SETTINGS = {'WEATHER_API_KEY': '', 'WEATHER_ZIP_CODE': '10001'}

Settings are stored per-plugin (user/webui/plugins/{name}.json) so they can't technically collide, but prefixing keeps things readable in logs and the Settings UI.


Multiple Functions

A single tool file can provide multiple functions:

ENABLED = True
AVAILABLE_FUNCTIONS = ['convert_temp', 'convert_length']

TOOLS = [
    {
        "type": "function",
        "is_local": True,
        "function": {
            "name": "convert_temp",
            "description": "Convert between Celsius and Fahrenheit.",
            "parameters": {
                "type": "object",
                "properties": {
                    "value": {"type": "number", "description": "Temperature value"},
                    "to": {"type": "string", "description": "'celsius' or 'fahrenheit'"}
                },
                "required": ["value", "to"]
            }
        }
    },
    {
        "type": "function",
        "is_local": True,
        "function": {
            "name": "convert_length",
            "description": "Convert between meters and feet.",
            "parameters": {
                "type": "object",
                "properties": {
                    "value": {"type": "number", "description": "Length value"},
                    "to": {"type": "string", "description": "'meters' or 'feet'"}
                },
                "required": ["value", "to"]
            }
        }
    }
]

def execute(function_name, arguments, config):
    if function_name == 'convert_temp':
        value = arguments.get('value', 0)
        to = arguments.get('to', 'celsius')
        if to == 'celsius':
            return f"{(value - 32) * 5/9:.1f}°C", True
        return f"{value * 9/5 + 32:.1f}°F", True

    if function_name == 'convert_length':
        value = arguments.get('value', 0)
        to = arguments.get('to', 'meters')
        if to == 'meters':
            return f"{value * 0.3048:.2f}m", True
        return f"{value / 0.3048:.2f}ft", True

    return f"Unknown function: {function_name}", False

Optional Flags

Export Type Default Purpose
EMOJI str Display icon (e.g. '🌤️')
is_local bool or str True True = offline, False = needs network, "endpoint" = calls external API
network bool False Mark as network-dependent (highlighted in UI, routed through SOCKS proxy)
EMOJI = '🌤️'

TOOLS = [{
    "type": "function",
    "is_local": False,       # uses network
    "network": True,          # route through SOCKS if configured
    "function": { ... }
}]

Lazy Imports

For heavy dependencies, import inside execute() so they only load when called:

def execute(function_name, arguments, config):
    if function_name == 'analyze':
        import pandas as pd  # only loaded when tool is called
        ...

Validation Levels

Tool Maker validates code before saving. The level is set in Settings > Tool Maker.

Level What's checked
Strict (default) ~60 allowlisted imports: requests, json, numpy, openai, anthropic, PIL, datetime, math, re, csv, bs4, hashlib, ssl, etc. Covers most tools without needing to change level.
Moderate Everything in strict + subprocess + any import not explicitly dangerous (shutil, ctypes, multiprocessing, socket, signal, importlib still blocked)
System Killer Syntax check only — no import or call restrictions. AI code can do anything on your system.

Tool Maker Commands

Tool What it does
tool_save(name, code) Validate and save a tool plugin
tool_load() Discover and activate new plugins (live, no restart)
tool_read(name?) Read source code, or list all AI-created plugins

Workflow

  1. Call tool_save("weather", code) — validates, creates user/plugins/weather/
  2. Call tool_load() — rescan picks it up, tool is immediately available
  3. The tool shows up in the plugin list and can be enabled/disabled like any plugin

Name Rules

  • Short and descriptive — 1-2 words (e.g. weather, unit_converter, stock_price)
  • The name becomes the plugin title in the UI (underscores → spaces, title cased)
  • Alphanumeric and underscores only
  • Cannot match core tool names (memory, knowledge, goals, web, meta, etc.)
  • Cannot match system plugin names (ssh, bitcoin, email, homeassistant, toolmaker, etc.)
  • Cannot start with underscore

What Tool Maker Does NOT Create

Tool Maker creates tool plugins — Python files the AI can call. For anything beyond that, a developer creates a full plugin manually. See the Plugin Author Guide.

Not supported by Tool Maker:

  • Hooks (pre_chat, prompt_inject, post_chat, etc.)
  • Voice commands (keyword triggers that bypass the LLM)
  • Scheduled tasks (cron jobs)
  • Web settings UI (custom JavaScript — though SETTINGS dict auto-renders without JS)

Reference for AI

TOOL MAKER — creates custom tool plugins at runtime.

WORKFLOW: tool_save(name, code) → tool_load() → tool is live

TEMPLATE:

ENABLED = True
EMOJI = '🔧'  # Pick an emoji that fits the tool
AVAILABLE_FUNCTIONS = ['my_func']

TOOLS = [
    {
        "type": "function",
        "is_local": True,
        "function": {
            "name": "my_func",
            "description": "What this tool does and when to use it.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "The input"}
                },
                "required": ["query"]
            }
        }
    }
]

def execute(function_name, arguments, config, plugin_settings=None):
    if function_name == 'my_func':
        query = arguments.get('query', '')
        return f"Result: {query}", True
    return f"Unknown function: {function_name}", False

RULES:

  • execute() returns (string, bool) — (result_text, success)
  • plugin_settings = dict of THIS plugin's settings from Settings UI
  • description field is critical — AI uses it to decide WHEN to call
  • EMOJI = required, pick one that fits the tool's purpose
  • is_local: True=offline, False=network
  • network: True = routed through SOCKS proxy
  • Lazy imports for heavy deps (import inside execute, not at top)
  • No parameters: "properties": {}, "required": []

SETTINGS (optional — adds fields to Settings UI):

SETTINGS = {'MYPLUGIN_API_KEY': '', 'MYPLUGIN_TIMEOUT': 30, 'MYPLUGIN_ENABLED': True}
SETTINGS_HELP = {'MYPLUGIN_API_KEY': 'Your API key', 'MYPLUGIN_TIMEOUT': 'Timeout in seconds'}

# Read in execute():
settings = plugin_settings or {}
api_key = settings.get('MYPLUGIN_API_KEY', '')
  • Prefix keys with plugin name for clarity
  • Types inferred from defaults: str→text, int/float→number, bool→toggle

MULTIPLE FUNCTIONS: Add entries to both TOOLS and AVAILABLE_FUNCTIONS, branch in execute().

ALLOWED IMPORTS (strict mode): requests, json, re, datetime, math, random, csv, base64, hashlib, uuid, numpy, PIL, bs4, openai, anthropic, tiktoken, pypdf, croniter, urllib, urllib3, http, ssl, collections, itertools, functools, typing, enum, dataclasses, copy, os, io, pathlib, logging, time, gzip, zlib, and more (~60 total)

NAME RULES: short (weather, stock_price), alphanumeric + underscores, no core/system names

AFTER SAVE: always call tool_load() to activate