Skip to main content
Bots can wire button clicks and select-menu choices to your own async handlers — across Telegram, Discord, and Slack — through a small registry API.

Quick Start

1

Use a built-in slash-command button (zero config)

Buttons wired to slash commands fire automatically — no handler registration needed.
import os
import asyncio
from praisonaiagents import Agent
from praisonaiagents.bots import (
    PresentationButton,
    PresentationAction,
    ActionType,
    MessagePresentation,
    PresentationBlock,
)
from praisonai.bots import TelegramBot

agent = Agent(
    name="assistant",
    instructions="You are a helpful assistant. When asked for help, show a help button.",
)

bot = TelegramBot(
    token=os.environ["TELEGRAM_BOT_TOKEN"],
    agent=agent,
)

help_button = PresentationButton(
    label="📚 Get Help",
    action=PresentationAction(type=ActionType.COMMAND, command="help"),
)

asyncio.run(bot.start())
When the user taps Get Help, the existing /help handler fires automatically via the built-in command namespace.
2

Register a custom namespace handler

For your own button logic, create a registry and register an async handler.
import os
import asyncio
from praisonaiagents import Agent
from praisonaiagents.bots import (
    InteractiveContext,
    create_registry,
    encode_action,
    PresentationAction,
    PresentationButton,
    ActionType,
)
from praisonai.bots import TelegramBot

agent = Agent(
    name="assistant",
    instructions="You are a helpful assistant.",
)

registry = create_registry()

async def on_approval(ctx: InteractiveContext) -> str | None:
    payload = ctx.platform_data.get("decoded_payload", {})
    choice = payload.get("value")
    if choice == "yes":
        return "approved"
    elif choice == "no":
        return "denied"
    return None

registry.register("approval", on_approval)

approve_button = PresentationButton(
    label="✅ Approve",
    action=PresentationAction(type=ActionType.CALLBACK, value="yes"),
)
deny_button = PresentationButton(
    label="❌ Deny",
    action=PresentationAction(type=ActionType.CALLBACK, value="no"),
)

approve_callback = encode_action("approval", approve_button.action)
deny_callback = encode_action("approval", deny_button.action)

bot = TelegramBot(
    token=os.environ["TELEGRAM_BOT_TOKEN"],
    agent=agent,
)

asyncio.run(bot.start())
encode_action("approval", ...) produces "approval:yes" / "approval:no". When a user taps the button, registry.dispatch() routes the click to on_approval.

How It Works

StepWhat happens
Button tapPlatform sends callback_data string to your bot
Adapter wrapsBuilds an InteractiveContext with user_id, message_id, chat_id, and platform-native objects
Registry decodesdecode_callback(callback_data) extracts namespace and payload
Handler runsAsync handler for matching namespace is called
FallbackIf no match or handler returns None, the fallback handler is tried

Decoding Rules

decode_callback(data) maps raw callback_data to a (namespace, payload) tuple:
InputNamespacePayload
"cmd:help""command"{"command": "help"}
"approval:yes""approval"{"value": "yes"}
"pair:approve:telegram:123:abc:deadbeef""pair"{"value": "approve:telegram:123:abc:deadbeef"}
"plain""plain"{}
"""unknown"{}
After dispatch(), the registry also writes decoded_namespace and decoded_payload into ctx.platform_data before calling your handler.

Choosing a Namespace Style


Platform-Specific Context

Each adapter populates ctx.platform_data with native objects so you can access full platform functionality inside your handler.
KeyTypeDescription
updatetelegram.UpdateFull update object
contexttelegram.ext.ContextTypes.DEFAULT_TYPEPTB context
querytelegram.CallbackQueryThe callback query that triggered the action
Access example:
async def my_handler(ctx: InteractiveContext) -> str | None:
    query = ctx.platform_data["query"]
    await query.answer("Processing...")
    return "handled"
KeyTypeDescription
interactiondiscord.InteractionThe interaction object
Access example:
async def my_handler(ctx: InteractiveContext) -> str | None:
    interaction = ctx.platform_data["interaction"]
    await interaction.edit_original_response(content="Done!")
    return "handled"
KeyTypeDescription
bodydictFull Block Kit action payload
actiondictThe specific action that was triggered
Access example:
async def my_handler(ctx: InteractiveContext) -> str | None:
    body = ctx.platform_data["body"]
    action = ctx.platform_data["action"]
    channel = body.get("channel", {}).get("id")
    return "handled"

Common Patterns

A button that fires an existing slash command — no handler registration needed.
import os
import asyncio
from praisonaiagents import Agent
from praisonaiagents.bots import (
    PresentationButton,
    PresentationAction,
    ActionType,
)
from praisonai.bots import SlackBot

agent = Agent(
    name="assistant",
    instructions="You are a helpful assistant.",
)

status_button = PresentationButton(
    label="📊 Check Status",
    action=PresentationAction(type=ActionType.COMMAND, command="status"),
)

bot = SlackBot(
    token=os.environ["SLACK_BOT_TOKEN"],
    app_token=os.environ["SLACK_APP_TOKEN"],
    agent=agent,
)

asyncio.run(bot.start())
Clicking Check Status triggers the built-in /status command automatically.

API Reference

InteractiveContext

Passed to every handler by the registry’s dispatch() method.
FieldTypeDefaultDescription
callback_datastrrequiredRaw callback data from the platform
user_idstrrequiredPlatform-native user ID
message_idOptional[str]NoneID of the message containing the button
chat_idOptional[str]NoneID of the chat where the click happened
bot_adapterOptional[BotAdapter]NoneThe bot adapter handling this interaction
platform_dataDict[str, Any]{}Platform-specific objects + decoded fields after dispatch
After dispatch(), platform_data also contains:
  • decoded_namespace — the decoded namespace string
  • decoded_payload — the decoded payload dict

InteractiveHandler

InteractiveHandler = Callable[[InteractiveContext], Awaitable[Optional[str]]]
Return a non-None string to mark the click as handled (stops the chain). Return None to fall through to the fallback handler.

InteractiveRegistry

MethodDescription
register(namespace, handler)Register an async handler for a namespace. Re-registering warns and overwrites.
unregister(namespace)Remove a handler. No-op if not registered.
set_fallback(handler)Register a fallback called when no namespace matches or the handler returns None.
dispatch(context)Decode callback_data, invoke the matching handler, try fallback. Returns True if handled.
has_handler(namespace)Check if a namespace has a registered handler.
list_namespaces()Return all registered namespace names.

Functions

FunctionSignatureDescription
encode_action(namespace, action)(str, PresentationAction) -> strEncode an action for callback_data. CALLBACK"ns:value", COMMAND"cmd:name", URL/WEB_APPnamespace.
decode_callback(data)(str) -> Tuple[str, Dict]Decode callback_data into (namespace, payload).
create_registry()() -> InteractiveRegistryPreferred. Create a fresh per-adapter registry.

Built-in Namespaces

Every platform adapter (Telegram, Discord, Slack) registers these automatically:
NamespaceWhat it does
commandRoutes cmd:<name> button clicks to the registered /name slash-command handler. Telegram also enforces command_policy.
pairHandles approve/deny buttons for unknown-user pairing (same behavior as before, now routed through InteractiveRegistry).

Best Practices

Each bot adapter should have its own registry to prevent namespace collisions when running multiple bots in the same process.
from praisonaiagents.bots import create_registry

registry = create_registry()
registry.register("approval", on_approval)
get_registry(), register_handler(), and unregister_handler() operate on a process-global registry and are deprecated. Multiple adapters sharing one registry can cause namespace conflicts. Always use create_registry() for new code.
Inline-button callbacks are HMAC-signed. Without a persistent secret, callbacks fail after every bot restart.
export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
Returning None lets the fallback handler run. This is useful when one namespace handler is shared across multiple button types and you only want to handle specific values.
async def on_menu(ctx: InteractiveContext) -> str | None:
    choice = ctx.platform_data.get("decoded_payload", {}).get("value", "")
    if choice in ("report", "settings"):
        return f"menu:{choice}"
    return None
Unhandled exceptions inside a handler are logged, but the registry will still try the fallback. Users won’t see a useful error message unless you handle it yourself.
async def on_approval(ctx: InteractiveContext) -> str | None:
    try:
        result = await process_approval(ctx)
        return result
    except Exception as e:
        query = ctx.platform_data.get("query")
        if query:
            await query.answer(f"Error: {e}")
        return "error"

Messaging Bots

Complete bot configuration and platform setup

Unknown-User Pairing

Owner-approval flow for new users using the pair namespace