Write a plugin
A plugin is a Python class that implements the BasePlugin protocol
and lives under app/plugins/builtin/. Once registered, it
contributes one or more tools that AIs in your tenant can call.
We'll build scaiwave.greeter — a toy plugin with one tool,
greet, that says hello in N languages.
1. Pick a plugin id
The id is <owner>.<name>. Built-in plugins use scaiwave.*; if
you're building a third-party plugin use your namespace (acme.crm,
bigco.sso, etc.). It must be unique per tenant.
2. Create the file
app/plugins/builtin/greeter.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 | """Greeter plugin — says hello in N languages."""
import json
from app.plugins.base import BasePlugin
from app.plugins.protocol import (
PluginContext,
PluginManifest,
ToolDefinition,
)
class GreeterPlugin(BasePlugin):
@property
def manifest(self) -> PluginManifest:
return PluginManifest(
id="scaiwave.greeter",
name="Greeter",
version="1.0.0",
description="Say hello in N languages.",
tools=[
ToolDefinition(
name="greet",
description=(
"Say hello in the given language. "
"Use when the user explicitly asks for a greeting "
"in a specific language, not as small talk."
),
usage="[/greet <language>]",
parameters={
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language code (en, de, nl, fr, ja, …).",
},
"name": {
"type": "string",
"description": "Whom to greet (optional).",
},
},
"required": ["language"],
},
),
],
)
async def get_system_context(self, ctx: PluginContext) -> str | None:
return None # No system-prompt injection needed for this plugin.
async def execute_tool(
self, tool_name: str, args: str, ctx: PluginContext,
) -> str:
if tool_name != "greet":
return ""
try:
parsed = json.loads(args) if isinstance(args, str) else args
except (ValueError, TypeError):
parsed = {"language": str(args).strip()}
language = (parsed.get("language") or "en").lower()
name = (parsed.get("name") or "world").strip()
greetings = {
"en": f"Hello, {name}!",
"de": f"Hallo, {name}!",
"nl": f"Hallo, {name}!",
"fr": f"Bonjour, {name}!",
"ja": f"こんにちは、{name}さん!",
}
return greetings.get(language, greetings["en"])
|
3. Register
Add to app/plugins/builtin/__init__.py:
| from app.plugins.builtin.greeter import GreeterPlugin
BUILTIN_PLUGINS = [
# … existing entries …
GreeterPlugin(),
]
|
Restart the server. The plugin is now discoverable.
4. Enable it
| curl -X POST "$BASE/v1/admin/plugins/scaiwave.greeter/enable" \
-H "Authorization: Bearer $ADMIN_TOKEN"
|
Or via the Admin → Plugins UI.
5. Test from chat
Send a message to an AI: "Say hello in German to Alice." The AI's
function-calling schema now includes greet; it should call it
with {"language": "de", "name": "Alice"} and respond with the
result.
What PluginContext carries
1
2
3
4
5
6
7
8
9
10
11
12
13 | @dataclass
class PluginContext:
db: AsyncSession # tenant-scoped SQLAlchemy session
tenant: TenantContext # current tenant
settings: Settings # config
redis: aioredis.Redis # for caching / coordination
room_id: str # current room id
sender_id: str # the AI participant making the call
user_token: str | None # the user's JWT (for token-exchange)
scaigrid: ScaiGridClient | None
arq: ArqRedis | None # for enqueueing background work
approval_service: ApprovalService | None
event_service_factory: Callable | None
|
You can read DB rows (always tenant-scoped), publish events, enqueue
ARQ jobs, etc.
Permissions
Declare in the manifest:
| permissions=[PluginPermission.EXTERNAL_HTTP, PluginPermission.READ_NOTES],
|
The framework checks before each call. Available:
READ_MESSAGES, WRITE_MESSAGES
READ_NOTES, WRITE_NOTES
EXTERNAL_HTTP
SEARCH
READ_USERS
MANAGE_TASKS
Side effects
If your tool mutates anything, set side_effects=True on the
ToolDefinition. In multi-user rooms, the framework can route
side-effect calls through an approval flow — a card with Approve
/ Reject appears in the chat before execution.
Fallback hints
When your tool errors, return a result that includes a "Fallback
suggestions" line. The AI's tool-etiquette rule tells it to try a
different angle rather than give up:
| return (
"Greeter failed: language not supported.\n"
"Fallback suggestions: `web_search` to find a greeting yourself."
)
|
Testing
Plugins are easy to test in isolation:
| # tests/unit/test_greeter_plugin.py
async def test_greet_returns_english_by_default(db, tenant_context):
plugin = GreeterPlugin()
ctx = PluginContext(
db=db, tenant=tenant_context, settings=Settings(),
redis=None, room_id="r", sender_id="ai",
)
out = await plugin.execute_tool("greet", '{"language":"en"}', ctx)
assert "Hello" in out
|
Where to go next