Plugin Development¶
Build a plugin in one Python file. Drop it in plugins/ — auto-loaded.
Minimal Plugin¶
# my_plugin.py
from simplecontext.plugins.base import BasePlugin
class MyPlugin(BasePlugin):
name = "my_plugin"
version = "1.0.0"
description = "What this plugin does."
def on_before_llm(self, user_id, agent_id, messages):
# Inject something into the system message
if messages and messages[0]["role"] == "system":
messages[0]["content"] += "\n\nExtra context here."
return messages
Drop it in plugins/ and restart. Done.
Full Plugin Reference¶
from simplecontext.plugins.base import BasePlugin, AppCommandContext
class MyPlugin(BasePlugin):
# ── Class attributes ──────────────────────────────────
name = "my_plugin" # unique, snake_case
version = "1.0.0"
description = "Short description."
depends_on = [] # ["other_plugin"] if needed
# ── App Commands (optional) ───────────────────────────
# Host apps (Telegram bot, Discord, CLI) read this and
# auto-register the commands. No changes to host code needed.
app_commands = {
"mycommand": {
"description": "What this command does",
"usage": "/mycommand <arg>",
"handler": "handle_mycommand", # method name below
"args_hint": "<query>",
"hidden": False,
}
}
# ── Lifecycle ─────────────────────────────────────────
def setup(self):
"""Called on init. self.state and self.config available."""
self.option = self.config.get("option", "default")
self.count = self.state.get("count", 0)
def teardown(self):
"""Called on shutdown. Clean up resources."""
self.state.set("count", self.count)
# ── App Command Handler ───────────────────────────────
async def handle_mycommand(self, ctx: AppCommandContext) -> str:
"""
Called by host app when user runs /mycommand.
ctx.user_id → str
ctx.args → list[str]
ctx.args_str → str (joined args)
ctx.platform → "telegram" | "discord" | "cli" | etc.
ctx.raw → raw platform object (e.g. Telegram Update)
ctx.sc → SimpleContext instance
"""
return f"Hello from {ctx.platform}! Query: {ctx.args_str}"
# ── Memory Hooks ─────────────────────────────────────
def on_message_saved(self, user_id, role, content, tags, metadata):
"""Fires after each message is stored."""
self.count += 1
self.state.increment("total_messages")
def on_messages_cleared(self, user_id):
"""Fires when user's memory is cleared."""
def on_context_build(self, user_id, messages: list) -> list:
"""
Fires when building context for LLM.
Modify, filter, or enrich messages.
MUST return list.
"""
return messages
# ── LLM Hooks ─────────────────────────────────────────
def on_before_llm(self, user_id, agent_id, messages: list) -> list:
"""
Fires just before sending to LLM.
Last chance to modify messages.
MUST return list.
"""
return messages
def on_after_llm(self, user_id, agent_id, response: str) -> str:
"""
Fires after LLM responds.
Modify, filter, or append to response.
MUST return str.
"""
return response
# ── Skill Hooks ───────────────────────────────────────
def on_skill_saved(self, agent_id, name, content): ...
def on_skill_deleted(self, agent_id, name): ...
def on_prompt_build(self, agent_id, prompt: str) -> str:
"""Fires after system prompt is built. MUST return str."""
return prompt
# ── Agent Hooks ───────────────────────────────────────
def on_agent_routed(self, user_id, agent_id, message): ...
def on_agent_chain(self, user_id, from_agent, to_agent, reason): ...
# ── Export/Import Hooks ───────────────────────────────
def on_export(self, data: dict) -> dict:
"""Fires on data export. MUST return dict."""
return data
def on_import(self, data: dict) -> dict:
"""Fires on data import. MUST return dict."""
return data
Plugin State¶
Plugins have access to self.state — a persistent key-value store backed by the same DB as SimpleContext:
# Set
self.state.set("key", value)
# Get (with default)
value = self.state.get("key", default)
# Increment integer counter
total = self.state.increment("counter") # → int
# Update multiple keys
self.state.update({"key1": val1, "key2": val2})
# Read all state for this plugin
all_data = self.state.all() # → dict
# Clear all state
self.state.clear()
State is namespaced per plugin — my_plugin.counter won't collide with other_plugin.counter.
Plugin Config¶
Users configure plugins in config.yaml:
Read in your plugin:
def setup(self):
self.option = self.config.get("option", "default")
self.threshold = float(self.config.get("threshold", 0.5))
App Commands¶
App commands let your plugin expose Telegram/Discord/CLI commands without touching the host app code.
app_commands = {
"stats": {
"description": "Show your usage statistics",
"usage": "/stats",
"handler": "handle_stats",
}
}
async def handle_stats(self, ctx: AppCommandContext) -> str:
total = self.state.get(f"messages:{ctx.user_id}", 0)
return f"📊 You've sent {total} messages."
The handler receives an AppCommandContext:
@dataclass
class AppCommandContext:
command: str # "stats"
user_id: str # "123456"
args: list[str] # ["arg1", "arg2"]
args_str: str # "arg1 arg2"
platform: str # "telegram" | "discord" | "cli" | "web"
raw: Any # platform-specific object (e.g. Telegram Update)
sc: Any # SimpleContext instance
extra: dict # additional data from host
Plugin Dependencies¶
If your plugin requires another plugin to be loaded first:
class MyPlugin(BasePlugin):
name = "my_plugin"
depends_on = ["logger_plugin"] # logger_plugin loads first
The loader resolves dependencies automatically via topological sort. If a dependency is missing, it raises a clear error.
Examples¶
Timestamp injector¶
from datetime import datetime, timezone
from simplecontext.plugins.base import BasePlugin
class TimestampPlugin(BasePlugin):
name = "timestamp_plugin"
version = "1.0.0"
description = "Inject current time into system message."
depends_on = []
def setup(self):
self.fmt = self.config.get("format", "%Y-%m-%d %H:%M UTC")
def on_before_llm(self, user_id, agent_id, messages):
now = datetime.now(timezone.utc).strftime(self.fmt)
if messages and messages[0]["role"] == "system":
messages[0]["content"] += f"\n\nCurrent time: {now}"
else:
messages.insert(0, {"role": "system", "content": f"Current time: {now}"})
self.state.increment("injections")
return messages
Usage logger¶
import json
from datetime import datetime, timezone
from simplecontext.plugins.base import BasePlugin
class LoggerPlugin(BasePlugin):
name = "logger_plugin"
version = "2.0.0"
description = "Log messages and track usage stats."
def setup(self):
self.log_path = self.config.get("log_path", "./sc.log")
def on_message_saved(self, user_id, role, content, tags, metadata):
total = self.state.increment("total_messages")
entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"user_id": user_id,
"role": role,
"preview": content[:80],
"total": total,
}
with open(self.log_path, "a") as f:
f.write(json.dumps(entry) + "\n")
def on_after_llm(self, user_id, agent_id, response):
self.state.increment("total_llm_calls")
return response
Configuration Reference¶
Set enabled: false to disable a plugin without removing its file.