MessagePresentation into a native payload for each channel. Built-in channels and pip-installed plugins register through the same seam — register_presentation_renderer — and unknown channels degrade gracefully to text.
Quick Start
1
Render for a known channel
2
Degrade unknown channels to text
3
Register a renderer from a plugin
Any pip-installed channel plugin plugs a native renderer into the core registry — no wrapper edits required.
How It Works
render_for(platform, presentation) resolves the platform’s renderer through the core registry and returns its native payload; unregistered channels fall back to fallback_text.
The registry lives in core (praisonaiagents.bots); the wrapper (praisonai_bot) only resolves through it and registers the four built-ins at import time.
The PresentationRendererProtocol
Every renderer implements PresentationRendererProtocol — two @staticmethod methods. The protocol is @runtime_checkable, so isinstance(cls, PresentationRendererProtocol) works.
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.
Both import paths are valid:
register_presentation_renderer and get_presentation_renderer are also exported from the top-level praisonaiagents.bots package — no need to reach into the presentation submodule.
Validation & Precedence
Registration fails loudly on misuse, and lookups are case-insensitive.
The wrapper registers each built-in only if the core slot is empty, so a plugin that registers first for
"telegram" wins — the built-in never clobbers it.
If the installed
praisonaiagents predates the seam, the wrapper silently falls back to its local built-in map — existing wrapper code keeps working on older cores.Built-in Renderers
The wrapper registers all four through the core seam at import time.
Graceful Degradation
fallback_text(presentation) keeps interactive content readable for channels without a renderer — nothing is silently dropped.
• Label bullets, and URL buttons inline as • Label: URL.
Add a Channel
Write a class with the two static methods, then register it through the public core seam —register_presentation_renderer.
render_for("mychannel", presentation) — and render_for("MyChannel", ...), render_for("MYCHANNEL", ...) — now all resolve the same renderer.
Best Practices
Register through the core seam, not a private dict
Register through the core seam, not a private dict
Call
register_presentation_renderer(platform, MyRenderer) from praisonaiagents.bots. That is the only registration visible to the resolution path everywhere else — a plugin renderer becomes indistinguishable from a built-in.Always adapt before mapping blocks
Always adapt before mapping blocks
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.Use render_for, not the class directly, in adapters
Use render_for, not the class directly, in adapters
render_for(platform, presentation) resolves the renderer and falls back to text for unknown platforms, so adapters stay channel-agnostic.Never drop interactive content
Never drop interactive content
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.Return the id shape your callback layer expects
Return the id shape your callback layer expects
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.
Related
Bot Presentations
The portable presentation model and per-channel limits
Bot Platform Plugins
Register a channel adapter and its native renderer as a pip-installable plugin
Message Presentation
Buttons, menus, and web-app links on agent replies
WhatsApp Bot
Native interactive rendering on WhatsApp

