Message Presentation & Inline Keyboards

Attach buttons, menus, and web-app links to agent replies; each platform renders them as native interactive widgets

Agents can return a MessagePresentation alongside their reply text — Telegram renders it as a native inline keyboard; other platforms fall back to plain text until their renderer is wired.

python
from praisonaiagents import Agent

agent = Agent(
    name="Survey Bot",
    instructions="Ask one multiple-choice question and attach button choices.",
)
agent.start("Which topic should we cover next: memory, tools, or workflows?")

The user receives a reply with inline buttons; their tap sends the choice back as the next message.

Diagram

Presentation Flow

text + presentation

text

blocks

Telegram

other

🤖 Agent

📡 Bot Adapter

💬 Chat Message

🎨 Platform\nRenderer

⌨️ Inline\nKeyboard

📝 Plain\nText

Quick Start

  1. Quick-Reply Buttons

    The simplest way to add buttons — each tapped choice feeds the value back to the agent as the next user message:

    python
    from praisonaiagents import Agent
    from praisonaiagents.bots import MessagePresentation, PresentationBlock
    
    agent = Agent(
        name="Survey Bot",
        instructions="""
        Ask a single multiple-choice question.
        Return your reply as text, then attach a presentation with buttons
        for the choices using PresentationBlock.quick_replies().
        """,
    )
    
    presentation = MessagePresentation(
        blocks=[
            PresentationBlock.make_text("How would you rate your experience?"),
            PresentationBlock.quick_replies([
                ("⭐ Excellent", "excellent"),
                ("👍 Good", "good"),
                ("👎 Poor", "poor"),
            ]),
        ]
    )
  2. One-line question builder

    MessagePresentation.question(...) is the shortest way to ask a multi-choice question — prompt, optional context, and option buttons in a single call:

    python
    from praisonaiagents import Agent
    from praisonaiagents.bots import MessagePresentation
    
    presentation = MessagePresentation.question(
        "Which environment should I deploy to?",
        options=[("🧪 Staging", "staging"), ("🚀 Production", "production"), "cancel"],
        context="Deployments run immediately.",
    )

    Each option becomes a reply-action button — a tap sends the chosen value back as the user’s next message, so no callback handler is needed.

  3. Inline Keyboard with Callbacks

    Full control over button appearance and callback data:

    python
    from praisonaiagents.bots import (
        MessagePresentation,
        PresentationBlock,
        PresentationButton,
        PresentationAction,
        ButtonStyle,
    )
    
    presentation = MessagePresentation(
        blocks=[
            PresentationBlock.make_text("Choose an action:"),
            PresentationBlock.make_buttons([
                PresentationButton(
                    label="✅ Approve",
                    action=PresentationAction.callback("approve:req-42"),
                    style=ButtonStyle.PRIMARY,
                    priority=10,
                ),
                PresentationButton(
                    label="❌ Deny",
                    action=PresentationAction.callback("deny:req-42"),
                    style=ButtonStyle.DANGER,
                    priority=9,
                ),
                PresentationButton(
                    label="🔗 Open Details",
                    action=PresentationAction.open_url("https://example.com/req/42"),
                    priority=8,
                ),
            ]),
        ]
    )
  4. Use with an Agent and Telegram Bot
    python
    import asyncio
    from praisonaiagents import Agent
    from praisonaiagents.bots import (
        MessagePresentation, PresentationBlock, PresentationButton,
        PresentationAction, ButtonStyle,
    )
    from praisonai.bots import TelegramBot
    
    agent = Agent(
        name="Action Bot",
        instructions="Help users take actions.",
    )
    
    bot = TelegramBot(token="YOUR_TOKEN", agent=agent)
    
    presentation = MessagePresentation(
        blocks=[
            PresentationBlock.make_text("What would you like to do?"),
            PresentationBlock.quick_replies([("Start", "start"), ("Help", "help")]),
        ]
    )
    
    async def main():
        await bot.start()
    
    asyncio.run(main())

How It Works

Diagram
TelegramRendererTelegramBotAgentTelegramRendererTelegramBotAgentcaptured presentation is cleared\n(no stale buttons on next turn)reply text + MessagePresentationsend text messagerender_presentation(presentation, limits)adapt_presentation() — truncate to limitssend InlineKeyboardMarkup

The adapt_presentation() function runs a channel-agnostic adaptation pass before rendering: it truncates buttons to platform limits, degrades select menus to button rows when unsupported, and degrades web_app actions to URLs on channels without mini-app support.


One-Line Question Builder

MessagePresentation.question(prompt, options, context=None) builds a prompt-plus-options presentation in a single call — the symmetric counterpart to MessagePresentation.approval(...) for non-binary “which of these?” clarifications.

python
from praisonaiagents import Agent
from praisonaiagents.bots import MessagePresentation

presentation = MessagePresentation.question(
    "Which environment should I deploy to?",
    options=[("🧪 Staging", "staging"), ("🚀 Production", "production"), "cancel"],
    context="Deployments run immediately.",
)

It composes a text prompt → optional context → a quick_replies button row. Every option carries PresentationAction.reply(value), so a tap feeds the chosen value straight into the next agent turn — no callback handler required.

ParameterTypeDefaultDescription
promptstrrequiredThe question text, rendered as the first text block.
optionsList[Any]requiredChoices as (label, value) tuples or plain strings (used as both label and value). Each becomes a reply-action button.
contextOptional[str]NoneOptional smaller context line rendered under the prompt via make_context.

Which builder to pick

Diagram

Allow / deny a tool

Pick one of N options

Complex layout with dividers,
mixed action types, etc.

What are you asking the user?

MessagePresentation.approval(…)

MessagePresentation.question(…)

Compose blocks manually
(make_text + quick_replies + …)

User interaction flow

Diagram
UserChannelBotAgentUserChannelBotAgentreply + MessagePresentation.question(“Which env?”, […])prompt + native option buttons“Which env? [Staging] [Production] [cancel]”tap Stagingreply action value = “staging”next turn input = “staging”

Block Types

Block typeMethodDescription
textPresentationBlock.make_text(content)Plain or markdown text
buttonsPresentationBlock.make_buttons(items)Row of clickable buttons
quick_repliesPresentationBlock.quick_replies(choices)Shortcut for reply-action buttons
selectPresentationBlock.make_select(options)Dropdown menu (degraded to buttons on Telegram)
dividerPresentationBlock.make_divider()Visual separator
contextPresentationBlock.make_context(content)Smaller contextual text
tablePresentationBlock.make_table(columns, rows)Tabular data — native table or markdown degrade (Table & Chart Blocks)
chartPresentationBlock.make_chart(chart_kind, series)Chart/visualisation — native chart or text-summary degrade (Table & Chart Blocks)
pollPresentationBlock.make_poll(question, options)Group vote — native poll or question + reply-buttons degrade (Poll Blocks)

Action Types

ActionFactoryDescription
callbackPresentationAction.callback(value)Opaque callback data for your handler
replyPresentationAction.reply(value)Feed value back as next agent input
commandPresentationAction.command("/cmd")Trigger a slash command
urlPresentationAction.open_url(url)Open a URL
web_appPresentationAction(type="web_app", web_app_url=url)Open a mini-app (Telegram only)

Platform Support & Limits

PlatformPresentation supportNotes
Telegram✅ Native inline keyboardWired in PR #2483
Slack🔜 PlannedFalls back to plain text
Discord🔜 PlannedFalls back to plain text
WhatsApp✅ Native interactive (reply buttons + list messages)Delivered via Cloud API interactive payloads (button / list) — see Bot Presentations

Telegram Limits

LimitValue
max_buttons per row/message8
max_button_rows100
max_button_label64 characters
callback_data64 UTF-8 bytes
max_text_length4096 characters
Native select menu❌ (degraded to buttons)
Web app (web_app_url)✅

WhatsApp Limits

LimitValue
max_buttons (list rows)10
Reply buttons per message3
max_button_rows1
max_button_label (list row)24 characters
Reply-button title20 characters
max_options (list rows)10
max_option_label24 characters
max_text_length (text / list body)4096 characters
Reply-button message body1024 characters
Reply / list-row id256 characters
Native select menu✅ (rendered as a list message)
Markdown❌
Web app (web_app_url)❌ (degraded via adapt_presentation)

Button Priorities

When more buttons are defined than the platform limit allows, the adaptation pass keeps the highest-priority buttons and drops the rest:

python
PresentationButton(label="Critical", action=..., priority=10)  # kept first
PresentationButton(label="Secondary", action=..., priority=5)   # dropped if cap hit
PresentationButton(label="Optional", action=..., priority=1)    # dropped first

Best Practices

Use reply actions for simple choices

PresentationAction.reply(value) feeds the chosen value back directly as the user’s next message — your agent sees it as normal input without needing any callback handler.

Use MessagePresentation.question(…) for a one-line clarification

Reach for question(...) when you want a prompt, optional context, and options in a single call. Compose blocks manually (make_text + quick_replies + …) only when you also need dividers, extra text blocks, or mixed action types.

Keep callback values short and ASCII

On Telegram this is now a performance / clarity recommendation, not a correctness one — long values still work, they simply take a store round-trip via the callback payload store. Telegram’s 64-byte callback cap is measured in UTF-8 bytes, so compact IDs (e.g. "approve:42") stay inline and skip the extra hop.

Presentations are cleared after each turn

The captured presentation is popped after chat() in both streaming and non-streaming paths, so cancelling or returning never leaves stale buttons visible.

Test with adapt_presentation()

Call adapt_presentation(presentation, PresentationLimits.telegram()) before sending to preview which buttons survive the Telegram limit and how select menus are degraded. Use PresentationLimits.whatsapp(), .slack(), or .discord() to preview the other channels.


Related