Phase 7 — Multi-modal Support
Goal
The agent can both receive and send media, and inline button presses round-trip back to the agent. After this phase, the bot is no longer text-only.
Scope / Deliverables
Inbound:
- Photo updates → download the largest size via Telegram
getFile, store metadata (file_id, dimensions, optional bytes path) inchat_messages.payload, forward normalized event to the agent - Document updates → same pattern; respect a size cap (e.g. 20MB) before downloading
- Callback queries (inline button presses) → normalized event includes
callbackData, surfaced to the agent; auto-ack withanswerCallbackQueryto clear the spinner - Decision: do we persist binary bytes or just
file_id? Phase-default =file_idonly (Telegram retains files), with an opt-in flag per tool to download.
Outbound:
-
send_imagesupports{url},{fileId}, and{bytesBase64, mimeType}sources -
send_document(same source variants) -
send_buttonsalready exists from Phase 2; extend to multi-row layouts and URL buttons
Automated Unit Testing
-
inbound.photo.test.ts—- photo update with multiple sizes → normalized event uses the largest size’s
file_idand dimensions chat_messages.payloadarchived row containsfile_id,width,height, and the raw normalized payload- by default no binary bytes are downloaded; with the opt-in flag enabled, the test exercises the download path (mock
getFile)
- photo update with multiple sizes → normalized event uses the largest size’s
-
inbound.document.test.ts—- document under the size cap → normalized event with
file_id,mime_type,file_size - document over the cap (e.g. 20MB+1) → event still surfaced to the agent (with metadata) but binary download skipped, audit/log entry noting the skip
- document under the size cap → normalized event with
-
callback.ack.test.ts—- inbound callback query → normalized event includes
callbackData,chatId,userId,messageId answerCallbackQueryis called exactly once for each callback (auto-ack), even if the agent forwarding step later fails
- inbound callback query → normalized event includes
-
outbound.image.test.ts—send_imagesource-variant handling:{url: string}→ telegraf called with{ url }{fileId: string}→ telegraf called with thefile_id{bytesBase64, mimeType}→ telegraf called with a Buffer/Uint8Array constructed from the base64; Zod rejects missingmimeType- any other shape →
INVALID_INPUT - all variants archive a
chat_messagesrow withkind='photo'and the source recorded inpayload
-
outbound.document.test.ts— same source-variant matrix forsend_document. -
outbound.buttons.test.ts—- multi-row layouts pass through correctly (2D
Button[][]) - URL buttons (
{label, url}) and callback buttons ({label, callbackData}) coexist in the same row - mixed row with invalid button shape →
INVALID_INPUT
- multi-row layouts pass through correctly (2D
Definition of Done
- Sending a photo to the bot → agent receives a normalized event with
file_idand dimensions. - Agent calls
send_image({bytesBase64})→ image appears in the chat. - Tapping an inline button on a bot message → agent receives a
callbackevent with the configuredcallbackData.