From fce1e333b9c6c2436081ad5132637bd03e5eb5b0 Mon Sep 17 00:00:00 2001 From: Flo Date: Fri, 3 Apr 2026 13:27:53 +0300 Subject: [PATCH] feat(telegram): render tool hints as expandable blockquotes (#2752) --- nanobot/channels/telegram.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index aaabd6468..1aa0568c6 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -29,6 +29,16 @@ TELEGRAM_MAX_MESSAGE_LEN = 4000 # Telegram message character limit TELEGRAM_REPLY_CONTEXT_MAX_LEN = TELEGRAM_MAX_MESSAGE_LEN # Max length for reply context in user message +def _escape_telegram_html(text: str) -> str: + """Escape text for Telegram HTML parse mode.""" + return text.replace("&", "&").replace("<", "<").replace(">", ">") + + +def _tool_hint_to_telegram_blockquote(text: str) -> str: + """Render tool hints as an expandable blockquote (collapsed by default).""" + return f"
{_escape_telegram_html(text)}
" if text else "" + + def _strip_md(s: str) -> str: """Strip markdown inline formatting from text.""" s = re.sub(r'\*\*(.+?)\*\*', r'\1', s) @@ -121,7 +131,7 @@ def _markdown_to_telegram_html(text: str) -> str: text = re.sub(r'^>\s*(.*)$', r'\1', text, flags=re.MULTILINE) # 5. Escape HTML special characters - text = text.replace("&", "&").replace("<", "<").replace(">", ">") + text = _escape_telegram_html(text) # 6. Links [text](url) - must be before bold/italic to handle nested cases text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', text) @@ -142,13 +152,13 @@ def _markdown_to_telegram_html(text: str) -> str: # 11. Restore inline code with HTML tags for i, code in enumerate(inline_codes): # Escape HTML in code content - escaped = code.replace("&", "&").replace("<", "<").replace(">", ">") + escaped = _escape_telegram_html(code) text = text.replace(f"\x00IC{i}\x00", f"{escaped}") # 12. Restore code blocks with HTML tags for i, code in enumerate(code_blocks): # Escape HTML in code content - escaped = code.replace("&", "&").replace("<", "<").replace(">", ">") + escaped = _escape_telegram_html(code) text = text.replace(f"\x00CB{i}\x00", f"
{escaped}
") return text @@ -460,8 +470,12 @@ class TelegramChannel(BaseChannel): # Send text content if msg.content and msg.content != "[empty message]": + render_as_blockquote = bool(msg.metadata.get("_tool_hint")) for chunk in split_message(msg.content, TELEGRAM_MAX_MESSAGE_LEN): - await self._send_text(chat_id, chunk, reply_params, thread_kwargs) + await self._send_text( + chat_id, chunk, reply_params, thread_kwargs, + render_as_blockquote=render_as_blockquote, + ) async def _call_with_retry(self, fn, *args, **kwargs): """Call an async Telegram API function with retry on pool/network timeout and RetryAfter.""" @@ -495,10 +509,11 @@ class TelegramChannel(BaseChannel): text: str, reply_params=None, thread_kwargs: dict | None = None, + render_as_blockquote: bool = False, ) -> None: """Send a plain text message with HTML fallback.""" try: - html = _markdown_to_telegram_html(text) + html = _tool_hint_to_telegram_blockquote(text) if render_as_blockquote else _markdown_to_telegram_html(text) await self._call_with_retry( self._app.bot.send_message, chat_id=chat_id, text=html, parse_mode="HTML",