Skip to main content
Bot platform adapters now ship in the praisonai-bot package. praisonai bot serve still works exactly as documented here; for a standalone install see praisonai-bot Migration.
Agents can reply with structured interactive messages — buttons, dropdowns, dividers, context text — and each channel adapter renders them as native widgets.
from praisonaiagents import Agent

agent = Agent(name="assistant", instructions="Offer clear yes/no choices in chat.")
agent.start("Ask the user to approve the deployment with buttons.")
The user taps an inline button or dropdown; each channel renders the same structured presentation natively.

Quick Start

1

Reply with two buttons

from praisonaiagents import Agent
from praisonaiagents.bots import (
    MessagePresentation,
    PresentationBlock,
    PresentationButton,
    PresentationAction,
)

def confirm_action(_args=None) -> MessagePresentation:
    return MessagePresentation(blocks=[
        PresentationBlock.make_text("Ready to deploy?"),
        PresentationBlock.make_buttons([
            PresentationButton(
                label="Deploy",
                action=PresentationAction(type="command", command="/deploy confirm"),
                style="primary",
                priority=10,
            ),
            PresentationButton(
                label="Cancel",
                action=PresentationAction(type="command", command="/deploy cancel"),
                style="danger",
                priority=9,
            ),
        ]),
    ])

agent = Agent(name="DeployBot", instructions="Confirm before deploying", tools=[confirm_action])
agent.start("Deploy main to production")
2

Dropdown selection

from praisonaiagents.bots import (
    MessagePresentation,
    PresentationBlock,
    SelectOption,
)

presentation = MessagePresentation(blocks=[
    PresentationBlock.make_text("Pick an environment:"),
    PresentationBlock.make_select(
        options=[
            SelectOption(label="Staging", value="staging", emoji="🧪"),
            SelectOption(label="Production", value="prod", emoji="🚀", default=True),
        ],
        placeholder="Choose an environment",
        action_id="env_select",
    ),
])
3

One-line approval prompt

from praisonaiagents.bots import MessagePresentation

presentation = MessagePresentation.approval(
    prompt="Allow delete_file for /var/log/old.log?",
    approval_id="appr_abc123",
    allow_always=True,
    context="Tool requested by agent: ops-bot",
)

How It Works

ChannelNative renderingIf unsupported
TelegramInline keyboardselect → button column
SlackBlock Kitweb_app action → URL button
DiscordComponentsweb_app action → URL button
WhatsAppReply buttons (≤3) / List messages (≤10 rows)Buttons >3 or select → list message; URL buttons inlined as text links

Block Types

BlockFactoryUse for
Textmake_text(content)Markdown body
Buttonsmake_buttons(items)Action rows
Selectmake_select(options)Dropdown menus
Dividermake_divider()Visual separator
Contextmake_context(content)Smaller hint text
Channel renderers always run adapt_presentation() first. Buttons over the per-channel cap are dropped lowest-priority first, so high-priority actions like Confirm or Deny survive on Discord and Slack.

Channel Adaptation

adapt_presentation() runs automatically inside every renderer — you can also call it yourself to preview what a channel will receive.
from praisonaiagents.bots import (
    MessagePresentation,
    PresentationBlock,
    PresentationButton,
    PresentationLimits,
    adapt_presentation,
)

buttons = [PresentationButton(label=f"opt {i}", priority=i) for i in range(12)]
presentation = MessagePresentation([PresentationBlock.make_buttons(buttons)])

adapted = adapt_presentation(presentation, PresentationLimits.slack())
# adapted.blocks[0].buttons → highest-priority 5 survive (opt 7..opt 11)
The input presentation is never mutated — adapt_presentation() always returns a new MessagePresentation.

Channel Limits

max_buttons is per-row capacity; total button cap is max_buttons × max_button_rows.
ChannelMax buttons (per row)Max button rowsTotal capMax button labelMax optionsMax option labelSupports selectSupports web_app
Telegram8100800640 (none)NoYes
Slack5157510075YesNo
Discord55258025100YesNo
WhatsApp10 (list rows) / 3 (reply buttons)110 (list) / 3 (reply)24 (list) / 20 (reply)1024Yes (native list)No
Telegram has no native select menu (supports_select=False). Any select block is automatically converted to a column of callback buttons by adapt_presentation().
WhatsApp uses split caps: ≤3 non-URL tappable buttons render as native reply buttons (title cap 20). More than 3 buttons — or any select block — promotes to a native list message (row title cap 24, up to 10 rows). URL buttons are never tappable widgets; their links are inlined into the message body as Label: URL lines.

Renderer Registry

Every channel plugs into a platform-keyed registry so adapters resolve a renderer uniformly and unknown channels degrade to readable text. render_for(platform, presentation) resolves the registered renderer and returns its native payload; channels with no renderer fall back to fallback_text(presentation).
from praisonai_bot.bots._presentation_renderer import (
    get_renderer,
    render_for,
    fallback_text,
)
from praisonaiagents.bots import MessagePresentation

presentation = MessagePresentation.approval("Allow file delete?", "appr-1")

# Resolve + render for a known channel
payload = render_for("whatsapp", presentation)   # native interactive payload

# Unknown channels degrade to readable text
render_for("email", presentation)                # → {"text": "...\n• Allow Once\n• Deny"}

get_renderer("slack")   # → SlackPresentationRenderer
get_renderer("email")   # → None
Each renderer implements the PresentationRenderer Protocol — two static methods, get_limits() and render(). Register a new channel by adding a class with these methods to _RENDERERS.
MethodReturnsPurpose
get_limits()PresentationLimitsChannel capability caps used by adapt_presentation()
render(presentation)Dict[str, Any]Native, platform-specific payload
fallback_text(presentation) flattens a presentation for channels without a registered renderer: text/context/divider blocks become lines, buttons and select options become • Label bullets, and URL buttons inline as • Label: URL so nothing is silently dropped.
See Presentation Renderers for the full registry API, the built-in renderer table, and an “add a channel” recipe.

Native rendering per channel

Call a renderer directly to preview the exact native payload a channel receives.
from praisonai_bot.bots._presentation_renderer import WhatsAppPresentationRenderer
from praisonaiagents.bots import MessagePresentation

rendered = WhatsAppPresentationRenderer.render(
    MessagePresentation.approval("Allow file delete?", "appr-1")
)
# rendered["interactive"]["type"] == "button"  → 2 tappable reply buttons

Capability Degradation

When a channel lacks a capability, adapt_presentation() gracefully degrades to the next best option. select → buttons on Telegram:
from praisonaiagents.bots import (
    MessagePresentation,
    PresentationBlock,
    PresentationLimits,
    SelectOption,
    adapt_presentation,
)

# Telegram has supports_select=False — adapt_presentation converts
# select options into a button column with bounded callback payloads.
block = PresentationBlock.make_select(
    options=[
        SelectOption(label="Staging", value="staging"),
        SelectOption(label="Production", value="prod"),
    ],
    action_id="env",
)
presentation = MessagePresentation([block])
adapted = adapt_presentation(presentation, PresentationLimits.telegram())
# → on Telegram, becomes two callback buttons:
#   [{label: "Staging", callback: "select:env:staging"},
#    {label: "Production", callback: "select:env:prod"}]
web_app → URL button on Slack/Discord:
from praisonaiagents.bots import (
    MessagePresentation,
    PresentationBlock,
    PresentationButton,
    PresentationAction,
    PresentationLimits,
    ActionType,
    adapt_presentation,
)

# Slack has supports_web_apps=False — adapt_presentation degrades a
# web_app action to a plain URL button so the link still works.
button = PresentationButton(
    label="Open App",
    action=PresentationAction(type=ActionType.WEB_APP, web_app_url="https://example.com"),
)
presentation = MessagePresentation([PresentationBlock.make_buttons([button])])
adapted = adapt_presentation(presentation, PresentationLimits.slack())
# → on Slack, becomes a URL button to https://example.com
For long action_id/value combinations, callback payloads are kept under 64 bytes using a SHA1 hash (16-hex truncation) so distinct options remain distinct after truncation.

Approval Prompts

On channels implementing SupportsPresentation, approval prompts render as inline Allow Once, Allow Always, and Deny buttons wired to /approve <approval_id> ... commands — replacing fragile yes/no text classification. Text-keyword backends (TelegramApproval, SlackApproval, DiscordApproval) remain valid fallbacks for channels without presentation support. When the underlying bot uses MessagePresentation.approval(...), the button namespace is automatically actor-bound — see Interactive Callback Authorization.

Best Practices

PresentationBlock.make_text() and make_buttons() are the agent-friendly path — fewer field mistakes than raw dataclass construction.
When a channel forces truncation, adapt_presentation() drops the lowest-priority buttons first. Give Confirm / Deny / Cancel high priority so they always reach the user.
Give Deny or Cancel higher priority so they survive truncation on Discord’s stricter component limits.
Always set MessagePresentation text content so channels without a registered renderer (e.g. Email) still deliver a readable message. WhatsApp now renders interactive presentations natively.
The built-in helper wires standard Allow/Deny buttons consistently across Telegram, Slack, and Discord.

Approval Protocol

Tool approval backends

Bot Gateway

Multi-channel gateway server

Bot Platform Capabilities

PresentationLimits governs interactive widgets; PlatformCapabilities governs message length, chunking, and editing.