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.
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.
Quick Start
- 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"), ]), ] ) - 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. - 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, ), ]), ] ) - 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
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.
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.
| Parameter | Type | Default | Description |
|---|---|---|---|
prompt | str | required | The question text, rendered as the first text block. |
options | List[Any] | required | Choices as (label, value) tuples or plain strings (used as both label and value). Each becomes a reply-action button. |
context | Optional[str] | None | Optional smaller context line rendered under the prompt via make_context. |
Which builder to pick
User interaction flow
Block Types
| Block type | Method | Description |
|---|---|---|
text | PresentationBlock.make_text(content) | Plain or markdown text |
buttons | PresentationBlock.make_buttons(items) | Row of clickable buttons |
quick_replies | PresentationBlock.quick_replies(choices) | Shortcut for reply-action buttons |
select | PresentationBlock.make_select(options) | Dropdown menu (degraded to buttons on Telegram) |
divider | PresentationBlock.make_divider() | Visual separator |
context | PresentationBlock.make_context(content) | Smaller contextual text |
table | PresentationBlock.make_table(columns, rows) | Tabular data — native table or markdown degrade (Table & Chart Blocks) |
chart | PresentationBlock.make_chart(chart_kind, series) | Chart/visualisation — native chart or text-summary degrade (Table & Chart Blocks) |
poll | PresentationBlock.make_poll(question, options) | Group vote — native poll or question + reply-buttons degrade (Poll Blocks) |
Action Types
| Action | Factory | Description |
|---|---|---|
callback | PresentationAction.callback(value) | Opaque callback data for your handler |
reply | PresentationAction.reply(value) | Feed value back as next agent input |
command | PresentationAction.command("/cmd") | Trigger a slash command |
url | PresentationAction.open_url(url) | Open a URL |
web_app | PresentationAction(type="web_app", web_app_url=url) | Open a mini-app (Telegram only) |
Platform Support & Limits
| Platform | Presentation support | Notes |
|---|---|---|
| Telegram | ✅ Native inline keyboard | Wired in PR #2483 |
| Slack | 🔜 Planned | Falls back to plain text |
| Discord | 🔜 Planned | Falls back to plain text |
| ✅ Native interactive (reply buttons + list messages) | Delivered via Cloud API interactive payloads (button / list) — see Bot Presentations |
Telegram Limits
| Limit | Value |
|---|---|
max_buttons per row/message | 8 |
max_button_rows | 100 |
max_button_label | 64 characters |
callback_data | 64 UTF-8 bytes |
max_text_length | 4096 characters |
| Native select menu | ❌ (degraded to buttons) |
Web app (web_app_url) | ✅ |
WhatsApp Limits
| Limit | Value |
|---|---|
max_buttons (list rows) | 10 |
| Reply buttons per message | 3 |
max_button_rows | 1 |
max_button_label (list row) | 24 characters |
| Reply-button title | 20 characters |
max_options (list rows) | 10 |
max_option_label | 24 characters |
max_text_length (text / list body) | 4096 characters |
| Reply-button message body | 1024 characters |
Reply / list-row id | 256 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:
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 firstBest 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
Durable approval buttons with actor authorisation
Bot PresentationsOverview of bot presentation capabilities
Interactive Bot ActionsHandling button taps and callbacks
Channel CapabilitiesWhat each platform can render
Table & Chart BlocksPortable table and chart blocks with per-channel rendering