Plugin Examples — Party Sampler
Written by ddxfish on .
- Docs
- Plugin Author Guide
- Plugin Examples
- All docs ↓
Real working examples of every plugin capability type, stripped to the minimum. Use these as templates. Each section is a complete working plugin.
1. Tools Plugin (simplest)
Two files. The AI gets new callable functions.
plugin.json:
{
"name": "my-tools",
"version": "1.0.0",
"description": "Example tools plugin",
"author": "you",
"capabilities": {
"tools": ["tools/my_tools.py"]
}
}
tools/my_tools.py:
ENABLED = True
EMOJI = '🔧'
AVAILABLE_FUNCTIONS = ['greet', 'add_numbers']
TOOLS = [
{
"type": "function",
"is_local": True,
"function": {
"name": "greet",
"description": "Greet someone by name",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Who to greet"}
},
"required": ["name"]
}
}
},
{
"type": "function",
"is_local": True,
"function": {
"name": "add_numbers",
"description": "Add two numbers",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"}
},
"required": ["a", "b"]
}
}
}
]
def execute(function_name, arguments, config):
if function_name == "greet":
return f"Hello, {arguments.get('name', 'world')}!", True
elif function_name == "add_numbers":
return f"{arguments['a']} + {arguments['b']} = {arguments['a'] + arguments['b']}", True
return "Unknown function", False
2. Hook Plugin (intercept the chat pipeline)
plugin.json:
{
"name": "my-hook",
"version": "1.0.0",
"description": "Injects context into every prompt",
"author": "you",
"capabilities": {
"hooks": {
"prompt_inject": "hooks/inject.py"
}
}
}
hooks/inject.py:
def prompt_inject(event):
"""Append extra context to the system prompt."""
event.context_parts.append("The user's favorite color is blue.")
Hook points: post_stt, pre_chat, prompt_inject, ghost_inject, post_llm, post_chat, pre_execute, post_execute, tools_filter, pre_tts, post_tts, on_wake, provider_switched, chat_renamed, chat_deleted, chat_cleared, chat_vaulted, plugins_ready — plus the streaming TTS hooks. Full table with fields and semantics: hooks.md
In a private chat your hooks are withheld unless the manifest declares top-level "privacy_aware": true — see Private chats.
Key fields on HookEvent:
event.input— user message (mutable in pre_chat)event.response— AI response (mutable in post_llm)event.skip_llm = True— bypass LLM entirelyevent.ephemeral = True— don't persist to chat historyevent.context_parts— list of strings appended to system promptevent.stop_propagation = True— prevent lower-priority hooks from firingevent.metadata.get("system")— access to VoiceChatSystem (stamped on most pipeline hooks: post_stt, pre_chat, ghost_inject, post_llm, post_chat, pre_execute, and the streaming TTS hooks — see the System Access table in hooks.md)
3. Voice Command Plugin (instant keyword response)
plugin.json:
{
"name": "my-voice-cmd",
"version": "1.0.0",
"description": "Custom voice commands",
"author": "you",
"capabilities": {
"voice_commands": [
{
"triggers": ["lights on", "turn on lights"],
"match": "exact",
"bypass_llm": true,
"handler": "hooks/lights.py"
}
]
}
}
hooks/lights.py:
def pre_chat(event):
# Do something (call an API, toggle a device, etc.)
event.skip_llm = True
event.ephemeral = True
event.response = "Lights turned on."
Match types: exact, starts_with, contains, regex
4. TTS Provider Plugin (add a speech engine)
plugin.json:
{
"name": "my-tts",
"version": "1.0.0",
"description": "Custom TTS provider",
"author": "you",
"settingsUI": null,
"capabilities": {
"providers": {
"tts": {
"key": "my_tts",
"display_name": "My TTS Engine",
"entry": "provider.py",
"class_name": "MyTTSProvider",
"requires_api_key": false
}
}
}
}
provider.py:
import logging
from core.tts.providers.base import BaseTTSProvider
logger = logging.getLogger(__name__)
class MyTTSProvider(BaseTTSProvider):
audio_content_type = 'audio/wav'
SPEED_MIN = 0.5
SPEED_MAX = 2.0
def generate(self, text, voice=None, speed=1.0, **kwargs):
"""Return audio bytes or None on failure."""
if not text or not text.strip():
return None
try:
# Your TTS generation here — subprocess, API call, etc.
# Must return raw audio bytes (WAV, MP3, etc.)
import subprocess
result = subprocess.run(
['your-tts-command', '--text', text],
capture_output=True, timeout=30
)
return result.stdout if result.returncode == 0 else None
except Exception as e:
logger.error(f"TTS generation failed: {e}")
return None
def is_available(self):
"""Return True if the TTS engine is installed/accessible."""
try:
import subprocess
subprocess.run(['your-tts-command', '--version'],
capture_output=True, timeout=5)
return True
except Exception:
return False
def list_voices(self):
"""Return available voices as [{"voice_id": "id", "name": "Display Name"}].
voice_id is what gets passed to generate(voice=). name is for the UI."""
return [
{"voice_id": "default", "name": "Default Voice"},
]
Important rules for TTS providers:
voice_idinlist_voices()must be the actual identifier your engine uses (language code, model name, etc.) — NOT a display name. Thenamefield is what the UI shows.- Audio output goes through an Opus encoder. WAV audio MUST be at an Opus-compatible sample rate: 8000, 12000, 16000, 24000, or 48000 Hz. If your engine outputs a different rate (e.g., 22050Hz), you must resample to 24000Hz before returning the bytes. Use
audioop.ratecv()+wavemodule for resampling. - MP3 audio (like gTTS/ElevenLabs) is handled separately and does not have this constraint.
- The
voiceparameter passed togenerate()comes from the voice picker UI. It will be avoice_idfrom yourlist_voices()return. Make sure the mapping is correct.
Provider types: tts, stt, embedding, llm — same manifest pattern, different base classes.
5. Settings (manifest-declared, auto-rendered)
Add to any plugin's capabilities. No JS needed — Sapphire renders the form automatically.
{
"capabilities": {
"settings": [
{
"key": "api_url",
"type": "string",
"label": "API URL",
"default": "http://localhost:8080",
"help": "The endpoint to connect to"
},
{
"key": "timeout",
"type": "number",
"label": "Timeout (seconds)",
"default": 30,
"min": 5,
"max": 120
},
{
"key": "mode",
"type": "string",
"label": "Mode",
"widget": "select",
"default": "normal",
"options": [
{"label": "Normal", "value": "normal"},
{"label": "Fast", "value": "fast"},
{"label": "Precise", "value": "precise"}
]
},
{
"key": "instructions",
"type": "string",
"widget": "textarea",
"label": "Custom Instructions",
"default": "",
"rows": 6
}
]
}
}
Read settings in your code:
from core.plugin_loader import plugin_loader
settings = plugin_loader.get_plugin_settings('my-plugin')
url = settings.get('api_url', 'http://localhost:8080')
6. Daemon (background thread with events)
plugin.json:
{
"name": "my-daemon",
"version": "1.0.0",
"description": "Background monitor with event triggers",
"author": "you",
"capabilities": {
"daemon": {
"entry": "daemon.py",
"event_sources": [
{
"name": "my_daemon_event",
"label": "My Daemon Event",
"description": "Fires when something happens"
}
]
}
}
}
daemon.py:
import logging
import threading
import time
logger = logging.getLogger(__name__)
_thread = None
_stop = threading.Event()
def start(plugin_loader, settings):
"""Called when plugin loads. Start your background thread."""
global _thread
_stop.clear()
_thread = threading.Thread(target=_monitor_loop, args=(plugin_loader,), daemon=True)
_thread.start()
logger.info("[my-daemon] Started")
def stop():
"""Called when plugin unloads. Clean shutdown."""
_stop.set()
logger.info("[my-daemon] Stopped")
def _monitor_loop(plugin_loader):
while not _stop.is_set():
# Check for something interesting
something_happened = False # your detection logic
if something_happened:
import json
plugin_loader.emit_daemon_event(
"my_daemon_event",
json.dumps({"text": "Something happened!", "detail": "..."})
)
_stop.wait(30) # check every 30 seconds
Daemon events trigger continuity tasks. Users create a task with the daemon event source, and Sapphire responds when the event fires.
7. Routes (custom API endpoints)
plugin.json:
{
"name": "my-routes",
"version": "1.0.0",
"description": "Custom API endpoints",
"author": "you",
"capabilities": {
"routes": [
{"method": "GET", "path": "status", "handler": "routes/api.py:get_status"},
{"method": "POST", "path": "action", "handler": "routes/api.py:do_action"}
]
}
}
routes/api.py:
import json
def get_status(body=None, settings=None, **_):
"""GET /api/plugin/my-routes/status — auto-authenticated, CSRF-protected."""
return {"status": "ok", "uptime": 12345}
def do_action(body=None, settings=None, **_):
"""POST /api/plugin/my-routes/action"""
action = (body or {}).get("action", "default")
return {"result": f"Did {action}"}
Routes are auto-mounted at /api/plugin/{plugin-name}/{path}. Auth and CSRF enforced by the framework.
8. Schedule (cron tasks)
plugin.json:
{
"name": "my-schedule",
"version": "1.0.0",
"description": "Scheduled tasks",
"author": "you",
"capabilities": {
"schedule": [
{
"name": "Daily Check",
"cron": "0 9 * * *",
"description": "Run a daily check at 9am",
"handler": "schedule/daily.py"
}
]
}
}
schedule/daily.py:
import logging
logger = logging.getLogger(__name__)
def run(event):
"""Called by the continuity scheduler on cron match."""
system = event.get("system")
config = event.get("config")
task = event.get("task")
state = event.get("plugin_state") # persistent key-value store
# Do your work here
logger.info("[my-schedule] Daily check running")
# Save state for next run
if state:
state.save("last_run", "2026-04-12")
Reference for AI
- Tool file exports:
ENABLED(bool),EMOJI(str),AVAILABLE_FUNCTIONS(list),TOOLS(OpenAI-style schemas),execute(function_name, arguments, config[, plugin_settings[, credentials]])→(message: str, success: bool). - Hooks:
capabilities.hooks={hook_name: "hooks/file.py"}; handler =def <hook_name>(event)ordef handle(event). Full hook list, mutable fields, and system-access table: hooks.md. - Voice commands:
capabilities.voice_commands=[{triggers, match: exact|starts_with|contains|regex, bypass_llm, handler}]; handler is apre_chatfunction. - Providers:
capabilities.providers.{tts|stt|embedding|llm}={key, display_name, entry, class_name, requires_api_key}. TTSgenerate()returns audio bytes or None; WAV must be at 8000/12000/16000/24000/48000 Hz (resample 22050 → 24000);list_voices()returns[{voice_id, name}]wherevoice_idis the engine's real identifier. - Settings:
capabilities.settingsschema auto-renders (types string/number, widgets password/select/textarea, min/max/options/rows); read viaplugin_loader.get_plugin_settings('my-plugin'). - Daemon:
capabilities.daemon={entry, event_sources: [{name, label, description}]}; entry exportsstart(plugin_loader, settings)/stop(); fire events viaplugin_loader.emit_daemon_event(name, json_string)— they trigger continuity tasks. - Routes:
capabilities.routes→/api/plugin/{plugin-name}/{path}; handler receives path params +body,settings,credentials,query,requestas kwargs — end signatures with**_. Details: routes.md. - Schedule:
capabilities.schedule=[{name, cron, description, handler}]; handler exportsrun(event)where event is a dict withsystem,config,task,plugin_state.
Edit this page on GitHub · docs/plugin-author/examples.md @ c2e3170 · synced
