Skip to main content
A platform-keyed registry turns one portable MessagePresentation into a native payload for each channel, and degrades gracefully to text when a channel has no renderer.

Quick Start

1

Render for a known channel

from praisonai_bot.bots._presentation_renderer import render_for
from praisonaiagents.bots import MessagePresentation

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

payload = render_for("whatsapp", presentation)
# payload["interactive"]["type"] == "button"  → native reply buttons
2

Degrade unknown channels to text

from praisonai_bot.bots._presentation_renderer import render_for
from praisonaiagents.bots import MessagePresentation

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

render_for("email", presentation)
# → {"text": "Allow file delete?\n• Allow Once\n• Deny"}

How It Works

render_for(platform, presentation) resolves the platform’s renderer from the registry and returns its native payload; unregistered channels fall back to fallback_text.
FunctionReturnsPurpose
get_renderer(platform)type or NoneLook up the registered renderer class
render_for(platform, presentation)Dict[str, Any]Render natively, else fall back to text
fallback_text(presentation)Dict[str, Any]Flatten to a readable {"text": ...} payload

The PresentationRenderer Protocol

Every renderer implements the PresentationRenderer Protocol — two static methods.
MethodReturnsPurpose
get_limits()PresentationLimitsChannel capability caps used by adapt_presentation()
render(presentation)Dict[str, Any]Native, platform-specific payload
Each renderer runs adapt_presentation against its own get_limits() before mapping blocks, so button overflow, unsupported selects/web-apps, and label truncation are applied uniformly.

Built-in Renderers

PlatformRendererPayload shape
telegramTelegramPresentationRenderer{"text", "reply_markup": {"inline_keyboard": ...}}
slackSlackPresentationRenderer{"blocks": [...]} (Block Kit)
discordDiscordPresentationRenderer{"content", "components": [...]}
whatsappWhatsAppPresentationRenderer{"text", "interactive": {"type": "button" | "list", ...}}

Graceful Degradation

fallback_text(presentation) keeps interactive content readable for channels without a renderer — nothing is silently dropped.
from praisonai_bot.bots._presentation_renderer import fallback_text
from praisonaiagents.bots import (
    MessagePresentation,
    PresentationBlock,
    PresentationButton,
    PresentationAction,
)

presentation = MessagePresentation(blocks=[
    PresentationBlock.make_text("Docs?"),
    PresentationBlock.make_buttons([
        PresentationButton(
            label="Open",
            action=PresentationAction.open_url("https://example.com"),
        ),
    ]),
])

fallback_text(presentation)
# → {"text": "Docs?\n• Open: https://example.com"}
Text/context/divider blocks become lines, buttons and select options become • Label bullets, and URL buttons inline as • Label: URL.

Add a Channel

Write a class with the two static methods, then register it into _RENDERERS.
from typing import Any, Dict
from praisonaiagents.bots import (
    MessagePresentation,
    PresentationLimits,
    adapt_presentation,
)
from praisonai_bot.bots._presentation_renderer import _RENDERERS

class MyChannelRenderer:
    @staticmethod
    def get_limits() -> PresentationLimits:
        return PresentationLimits()

    @staticmethod
    def render(presentation: MessagePresentation) -> Dict[str, Any]:
        presentation = adapt_presentation(presentation, MyChannelRenderer.get_limits())
        # map blocks to your channel's native payload
        return {"text": "..."}

_RENDERERS["mychannel"] = MyChannelRenderer
render_for("mychannel", presentation) now resolves your renderer.

Best Practices

Run adapt_presentation(presentation, get_limits()) first so button overflow, label caps, and unsupported selects/web-apps degrade uniformly before you build the native payload.
render_for(platform, presentation) resolves the renderer and falls back to text for unknown platforms, so adapters stay channel-agnostic.
When a channel can’t render a widget, surface it as text (labels, URLs) the way fallback_text does — a silently dropped button is worse than a text link.
Reply/list-row ids are how taps route back to your handler. Derive stable ids (command, callback value, or URL) and keep them within the channel’s id cap.

Bot Presentations

The portable presentation model and per-channel limits

Message Presentation

Buttons, menus, and web-app links on agent replies

WhatsApp Bot

Native interactive rendering on WhatsApp

Channel Capabilities

Live-edit, reactions, typing, and text limits per channel