Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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:

python
 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:

python
1
2
3
4
5
6
from app.plugins.builtin.greeter import GreeterPlugin

BUILTIN_PLUGINS = [
    # … existing entries …
    GreeterPlugin(),
]

Restart the server. The plugin is now discoverable.

4. Enable it#

bash
1
2
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#

python
 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:

python
1
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:

python
1
2
3
4
return (
    "Greeter failed: language not supported.\n"
    "Fallback suggestions: `web_search` to find a greeting yourself."
)

Testing#

Plugins are easy to test in isolation:

python
1
2
3
4
5
6
7
8
9
# 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#

Updated 2026-05-17 13:10:04 View source (.md) rev 1