Plugin Tutorial: Build a Rate Limiter¶
This tutorial walks through building a complete plugin from scratch — a rate limiter that restricts how frequently each user can send messages.
What We're Building¶
A plugin that: - Tracks message timestamps per user - Rejects messages if the user sends too fast - Exposes /ratelimit command to check current status - Is configurable via config.yaml
Step 1: Create the file¶
Step 2: Skeleton¶
Start with the minimum:
from simplecontext.plugins.base import BasePlugin
class RateLimiterPlugin(BasePlugin):
name = "rate_limiter_plugin"
version = "1.0.0"
description = "Limit message frequency per user."
Step 3: Config and setup¶
from simplecontext.plugins.base import BasePlugin
class RateLimiterPlugin(BasePlugin):
name = "rate_limiter_plugin"
version = "1.0.0"
description = "Limit message frequency per user."
def setup(self):
# How many messages allowed per window
self.max_messages = int(self.config.get("max_messages", 10))
# Window size in seconds
self.window_secs = int(self.config.get("window_seconds", 60))
# Message to send when rate limited
self.block_msg = self.config.get(
"block_message",
f"⚠️ Slow down! Max {self.max_messages} messages per minute."
)
Step 4: Track messages¶
Use self.state for persistent storage:
import time
def _get_timestamps(self, user_id: str) -> list:
"""Get message timestamps for a user."""
return self.state.get(f"ts:{user_id}", [])
def _save_timestamps(self, user_id: str, timestamps: list):
"""Save message timestamps."""
self.state.set(f"ts:{user_id}", timestamps)
def _is_rate_limited(self, user_id: str) -> bool:
"""Check if user has exceeded the rate limit."""
now = time.time()
window_start = now - self.window_secs
# Get timestamps within window
timestamps = self._get_timestamps(user_id)
recent = [ts for ts in timestamps if ts > window_start]
# Add current timestamp
recent.append(now)
self._save_timestamps(user_id, recent)
return len(recent) > self.max_messages
Step 5: Hook into the pipeline¶
Use on_before_llm to intercept messages before they reach the LLM:
def on_before_llm(self, user_id, agent_id, messages):
"""Block the request if user is rate limited."""
if self._is_rate_limited(user_id):
self.state.increment("total_blocked")
# Replace last user message with block message
for i in range(len(messages) - 1, -1, -1):
if messages[i]["role"] == "user":
messages[i]["content"] = self.block_msg
break
return messages
Step 6: Add an app_command¶
Let users check their current rate limit status:
from simplecontext.plugins.base import BasePlugin, AppCommandContext
import time
class RateLimiterPlugin(BasePlugin):
# ... (previous code) ...
app_commands = {
"ratelimit": {
"description": "Check your current message rate",
"usage": "/ratelimit",
"handler": "handle_ratelimit",
}
}
async def handle_ratelimit(self, ctx: AppCommandContext) -> str:
now = time.time()
window_start = now - self.window_secs
timestamps = self._get_timestamps(ctx.user_id)
recent_count = len([ts for ts in timestamps if ts > window_start])
remaining = max(0, self.max_messages - recent_count)
total_blocked = self.state.get("total_blocked", 0)
return (
f"📊 *Rate Limit Status*\n\n"
f"Messages this minute: `{recent_count}/{self.max_messages}`\n"
f"Remaining: `{remaining}`\n"
f"Window: `{self.window_secs}s`\n"
f"Total blocked (all users): `{total_blocked}`"
)
Step 7: Complete plugin¶
"""
rate_limiter_plugin.py — SimpleContext Plugin
Limit message frequency per user to prevent spam.
Config in config.yaml:
plugins:
rate_limiter_plugin:
enabled: true
max_messages: 10 # per window
window_seconds: 60 # window size in seconds
block_message: "⚠️ Too fast! Please slow down."
"""
import time
from simplecontext.plugins.base import BasePlugin, AppCommandContext
class RateLimiterPlugin(BasePlugin):
name = "rate_limiter_plugin"
version = "1.0.0"
description = "Limit message frequency per user."
depends_on = []
app_commands = {
"ratelimit": {
"description": "Check your current message rate",
"usage": "/ratelimit",
"handler": "handle_ratelimit",
}
}
def setup(self):
self.max_messages = int(self.config.get("max_messages", 10))
self.window_secs = int(self.config.get("window_seconds", 60))
self.block_msg = self.config.get(
"block_message",
f"⚠️ Slow down! Max {self.max_messages} messages per {self.window_secs}s."
)
def _get_timestamps(self, user_id: str) -> list:
return self.state.get(f"ts:{user_id}", [])
def _save_timestamps(self, user_id: str, timestamps: list):
self.state.set(f"ts:{user_id}", timestamps)
def _is_rate_limited(self, user_id: str) -> bool:
now = time.time()
window_start = now - self.window_secs
timestamps = self._get_timestamps(user_id)
recent = [ts for ts in timestamps if ts > window_start]
recent.append(now)
self._save_timestamps(user_id, recent)
return len(recent) > self.max_messages
def on_before_llm(self, user_id, agent_id, messages):
if self._is_rate_limited(user_id):
self.state.increment("total_blocked")
for i in range(len(messages) - 1, -1, -1):
if messages[i]["role"] == "user":
messages[i]["content"] = self.block_msg
break
return messages
async def handle_ratelimit(self, ctx: AppCommandContext) -> str:
now = time.time()
window_start = now - self.window_secs
timestamps = self._get_timestamps(ctx.user_id)
recent_count = len([ts for ts in timestamps if ts > window_start])
remaining = max(0, self.max_messages - recent_count)
blocked = self.state.get("total_blocked", 0)
return (
f"📊 *Rate Limit Status*\n\n"
f"Messages this window: `{recent_count}/{self.max_messages}`\n"
f"Remaining: `{remaining}`\n"
f"Window: `{self.window_secs}s`\n"
f"Total blocked: `{blocked}`"
)
Step 8: Test it¶
from simplecontext import SimpleContext
from plugins.rate_limiter_plugin import RateLimiterPlugin
sc = SimpleContext()
sc.use(RateLimiterPlugin(config={
"max_messages": 3,
"window_seconds": 10,
}))
# Verify it loaded
plugin = sc._plugins.get("rate_limiter_plugin")
print(plugin)
# → <Plugin name='rate_limiter_plugin' v1.0.0 enabled=True deps=[] commands=['ratelimit']>
# Check app_commands
print(plugin.get_app_commands())
# → {"ratelimit": {"description": "...", "handler": "handle_ratelimit"}}
Step 9: Configure and deploy¶
# config.yaml
plugins:
rate_limiter_plugin:
enabled: true
max_messages: 10
window_seconds: 60
block_message: "⚠️ Too many messages! Please wait a moment."
With SimpleContext-Bot, /ratelimit is automatically registered as a Telegram command.
What You Learned¶
- Plugin file structure and BasePlugin inheritance
- Using
self.configfor configuration - Using
self.statefor persistent per-user data on_before_llmhook for intercepting requestsapp_commandsandAppCommandContextfor bot commands- Testing plugin loading and introspection