Inbound Message Gate
Drop or redact incoming chat messages before they reach your agent
Filter, redact, or reject messages from Telegram, Slack, Discord, WhatsApp, and email before your agent sees them.
from praisonaiagents import Agent
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
from praisonai.bots import TelegramBot
registry = HookRegistry()
ALLOWED = {"12345", "67890"}
@registry.on(HookEvent.MESSAGE_RECEIVED)
def gate(event_data):
if event_data.sender_id not in ALLOWED:
return HookResult.deny("not on allowlist")
return HookResult.allow()
agent = Agent(name="GatedBot", instructions="Be helpful.", hooks=registry)
bot = TelegramBot(token="...", agent=agent)
import asyncio
asyncio.run(bot.start())Quick Start
- Allowlist — drop messages from unknown senders
python from praisonaiagents import Agent from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult ALLOWED = {"12345", "67890"} registry = HookRegistry() @registry.on(HookEvent.MESSAGE_RECEIVED) def gate(event_data): if event_data.sender_id not in ALLOWED: return HookResult.deny("not on allowlist") return HookResult.allow() agent = Agent(name="GatedBot", instructions="Be helpful.", hooks=registry) agent.start("Hello") - PII redaction — rewrite content before the LLM sees it
python import re from praisonaiagents import Agent from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult SSN = re.compile(r"\b\d{3}-\d{2}-\d{4}\b") registry = HookRegistry() @registry.on(HookEvent.MESSAGE_RECEIVED) def redact(event_data): cleaned = SSN.sub("[SSN]", event_data.content) if cleaned != event_data.content: return HookResult(decision="allow", modified_input={"content": cleaned}) return HookResult.allow() agent = Agent(name="SafeBot", instructions="Be helpful.", hooks=registry) agent.start("My SSN is 123-45-6789")
How It Works
Decision Table
| Hook returns | drop | content sent to agent | Effect |
|---|---|---|---|
HookResult.allow() (or no hook) | false | original | Message flows unchanged |
HookResult.deny("...") | true | — | Adapter skips dispatch — bot stays silent |
HookResult(decision="allow", modified_input={"content": "…"}) | false | redacted text | LLM sees rewritten content |
| Hook raises exception | false | original | Non-fatal — message passes through |
Supported Adapters
Every built-in messaging adapter honours the gate decision:
| Adapter | Module |
|---|---|
| Telegram | praisonai.bots.telegram |
| Slack | praisonai.bots.slack |
| Discord | praisonai.bots.discord |
praisonai.bots.whatsapp (regular + status paths) | |
praisonai.bots.email | |
| AgentMail | praisonai.bots.agentmail |
Slack @mentions
The Slack app_mention handler fires MESSAGE_RECEIVED before allow-list checks, so @mentions now flow through the same gate as DMs (previously DM-only).
| Hook decision | Effect on the @mention |
|---|---|
drop=True | Handler short-circuits — the mention is silently dropped. |
rewritten content | The LLM prompt uses the hook’s content, not the raw Slack event text. |
empty content | Honored verbatim — an intentional redaction to empty is respected. |
Mentions also now count toward channel last_activity and increment messages_inbound_total, so an @mention registers as inbound activity for /health.
Quoted content in the hook
On Telegram, when a user replies to or quotes an earlier message, event_input.content for MESSAGE_RECEIVED is the rendered turn — the quoted block above the new text — so a redaction/deny hook can inspect and veto quoted text too. If the hook rewrites content, the separately-resolved quoted reference is cleared so unredacted quoted text can’t be re-appended after the hook runs.
See Quoted Reply Context.
Common Patterns
Allowlist
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
ALLOWED = {"user_id_1", "user_id_2"}
registry = HookRegistry()
@registry.on(HookEvent.MESSAGE_RECEIVED)
def allowlist(event_data):
if event_data.sender_id not in ALLOWED:
return HookResult.deny("sender not allowed")
return HookResult.allow()Rate Limiter
import time
from collections import defaultdict
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
registry = HookRegistry()
_counts: dict = defaultdict(list)
LIMIT = 5
WINDOW = 60
@registry.on(HookEvent.MESSAGE_RECEIVED)
def rate_limit(event_data):
now = time.time()
timestamps = [t for t in _counts[event_data.sender_id] if now - t < WINDOW]
if len(timestamps) >= LIMIT:
return HookResult.deny("rate limit exceeded")
timestamps.append(now)
_counts[event_data.sender_id] = timestamps
return HookResult.allow()Keyword Ban
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
BANNED = {"spam", "scam"}
registry = HookRegistry()
@registry.on(HookEvent.MESSAGE_RECEIVED)
def keyword_ban(event_data):
for word in BANNED:
if word in event_data.content.lower():
return HookResult.deny("banned keyword")
return HookResult.allow()Combined Gate + Redaction
import re
from praisonaiagents import Agent
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
from praisonai.bots import TelegramBot
registry = HookRegistry()
BLOCKED = {"1234567"}
SSN = re.compile(r"\b\d{3}-\d{2}-\d{4}\b")
@registry.on(HookEvent.MESSAGE_RECEIVED)
def gate(event_data):
if event_data.sender_id in BLOCKED:
return HookResult.deny("blocked sender")
scrubbed = SSN.sub("[SSN]", event_data.content)
if scrubbed != event_data.content:
return HookResult(decision="allow", modified_input={"content": scrubbed})
return HookResult.allow()
agent = Agent(name="SafeBot", instructions="Be helpful.", hooks=registry)
bot = TelegramBot(token="...", agent=agent)
import asyncio
asyncio.run(bot.start())Behaviour:
- Blocked user’s messages: agent stays silent, no reply sent.
- Messages containing an SSN pattern: LLM sees
[SSN]instead of the digits. - All other messages: unchanged.
User Interaction Flow
A Telegram user sends a message. The gate runs before the agent responds:
| Message | sender_id | Hook result | Agent sees | Bot reply |
|---|---|---|---|---|
| “Hello!” | 12345 (allowed) | allow | “Hello!” | Normal response |
| “ping” | 99999 (blocked) | deny | — | (silent) |
| “SSN: 123-45-6789” | 12345 (allowed) | modified_input | “SSN: [SSN]” | Answers without digits |
Symmetry with Outbound
MESSAGE_RECEIVED (inbound) and MESSAGE_SENDING (outbound) share the same deny / modified_input contract. Use the same pattern for both directions:
@registry.on(HookEvent.MESSAGE_SENDING)
def redact_outbound(event_data):
cleaned = SSN.sub("[SSN]", event_data.content)
if cleaned != event_data.content:
return HookResult(decision="allow", modified_input={"content": cleaned})
return HookResult.allow()Best Practices
Keep the gate cheap
The hook runs on every inbound message. Avoid blocking I/O or heavy computation — use in-memory lookups and pre-compiled regex patterns.
Fail-open, not fail-closed
Raising an exception lets the message through. Explicitly call HookResult.deny(...) if you mean to block.
Mutate content only for redaction
The LLM will see the modified_input content and may quote it back in replies. Only rewrite when necessary (e.g. PII scrubbing).
Use deny for hard blocks, not redaction
deny prevents agent dispatch entirely. modified_input still dispatches — use it only when you want the agent to respond to the sanitised content.