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.
- AI calls
tool_save(name, code)— validates and saves asuser/plugins/{name}/ - AI calls
tool_load()— discovers and activates the new plugin live - 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.
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| 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) |
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— workedreturn "Error: something broke", False— failed, AI sees the errorreturn "No results found", True— empty result (not an error)
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"Standard JSON Schema types: string, integer, number, boolean, array, object.
No parameters:
"parameters": {"type": "object", "properties": {}, "required": []}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}", FalseSETTINGSdict in your code → Tool Maker auto-converts to manifestcapabilities.settings- Settings appear in web UI under the plugin's settings tab (auto-rendered, no JavaScript needed)
- Read at runtime via the
plugin_settingsparameter inexecute()— no imports needed SETTINGS_HELPdict (optional) adds descriptions below each field
Type inference from defaults:
str→ text inputintorfloat→ number spinnerbool→ toggle switch
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.
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| 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": { ... }
}]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
...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 | 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 |
- Call
tool_save("weather", code)— validates, createsuser/plugins/weather/ - Call
tool_load()— rescan picks it up, tool is immediately available - The tool shows up in the plugin list and can be enabled/disabled like any plugin
- 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
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)
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}", FalseRULES:
- 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