Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions common/telegram_markdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Shared Telegram markdown/code formatting helpers.

This module lives outside `plugins/` so individual plugins can remain
self-contained and not depend on other plugins being installed.
"""

from __future__ import annotations

import re


def preserve_telegram_markdown(text: str) -> str:
"""Preserve common markdown constructs while keeping Telegram compatibility.

Notes:
- We intentionally keep this conservative to avoid over-escaping and
flattening newlines (which breaks fenced code blocks).
- The behavior here is expected to be shared by both Telegram integrations.
"""

if not text:
return text

# Convert _italic_ to __italic__ (Telegram-style italics)
text = re.sub(r"_([^_]+)_", r"__\1__", text)

# Convert *italic* to __italic__ while preserving **bold**
text = re.sub(r"\*\*([^*]+)\*\*", r"<BOLD>\1</BOLD>", text)
text = re.sub(r"\*([^*]+)\*", r"__\1__", text)
text = re.sub(r"<BOLD>([^<]+)</BOLD>", r"**\1**", text)

# Convert non-standard ..code.. delimiter into fenced blocks
text = re.sub(r"\.\.\n(.*?)\.\.", r"```\n\1\n```", text, flags=re.DOTALL)

# Quote blocks: render as a subtle label (Telegram markdown support varies)
text = re.sub(r"^>\s*(.*?)$", r"*Quote:* \1", text, flags=re.MULTILINE)

return text.strip()

Binary file added core
Binary file not shown.
32 changes: 2 additions & 30 deletions plugins/telegram/message_handler.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Telegram message handler implementation."""

import asyncio
import re
from datetime import datetime
from typing import Any

from common.telegram_markdown import preserve_telegram_markdown
from database.operations.messages import insert_message
from database.operations.queue import add_to_queue
from database.operations.users import get_or_create_platform_profile
Expand Down Expand Up @@ -44,35 +44,7 @@ def preserve_markdown(self, text: str) -> str:
Returns:
str: Text with preserved markdown formatting
"""
if not text:
return text

# Simple, direct approach - just preserve the original formatting
# Don't over-process or convert unnecessarily

# Only handle the specific cases that Telegram has trouble with
# Convert _italic_ to __italic__ (Telegram uses double underscores)
# Remove word boundary requirement - it's too restrictive
text = re.sub(r"_([^_]+)_", r"__\1__", text)

# Convert *italic* to __italic__ (standardize to Telegram format)
# But be careful not to break **bold** - only convert single asterisks
# First protect bold patterns
text = re.sub(r"\*\*([^*]+)\*\*", r"<BOLD>\1</BOLD>", text)
# Then convert remaining single asterisks
text = re.sub(r"\*([^*]+)\*", r"__\1__", text)
# Finally restore bold patterns
text = re.sub(r"<BOLD>([^<]+)</BOLD>", r"**\1**", text)

# Handle non-standard code block delimiters
# Convert ..code.. to ```code```
text = re.sub(r"\.\.\n(.*?)\.\.", r"```\n\1\n```", text, flags=re.DOTALL)

# Handle quote blocks - convert to italic prefix (more subtle)
text = re.sub(r"^>\s*(.*?)$", r"*Quote:* \1", text, flags=re.MULTILINE)

# That's it - preserve everything else as-is
return text.strip()
return preserve_telegram_markdown(text)


class MessageBuffer:
Expand Down
22 changes: 19 additions & 3 deletions plugins/telegram_bot/message_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@
import logging
from typing import Any

from common.telegram_markdown import preserve_telegram_markdown
from database.operations.messages import insert_message
from database.operations.queue import add_to_queue
from database.operations.users import get_or_create_platform_profile
from runtime.core.message import MessageFormatter
from runtime.core.message import MessageFormatter as BaseMessageFormatter

logger = logging.getLogger(__name__)


class MessageFormatter(BaseMessageFormatter):
"""Telegram-bot-specific formatter (kept self-contained).

We keep Telegram markdown behavior aligned with the Telethon-based Telegram
plugin by delegating to shared helpers in `common/`.
"""

def format_response(self, response: str) -> str:
return preserve_telegram_markdown(response)


class TelegramMessageHandler:
"""Handles incoming and outgoing messages for the Telegram bot."""

Expand Down Expand Up @@ -86,8 +98,12 @@ async def process_outgoing_message(self, message, response: str) -> None:
response: The response to send
"""
try:
# Send response
await message.answer(response)
# Format response for Telegram (preserve markdown/code blocks)
formatted = self.formatter.format_response(response)

# Send response with markdown enabled (match Telegram plugin behavior)
# Use Telegram "Markdown" to align with the Telethon plugin's parse_mode="markdown".
await message.answer(formatted, parse_mode="Markdown")

# Update message status
await self.update_message_status(message, "sent")
Expand Down
12 changes: 7 additions & 5 deletions plugins/telegram_bot/tests/test_markdown_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
# Add the broca2 directory to the path so we can import modules
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from plugins.telegram.message_handler import MessageFormatter
from common.telegram_markdown import preserve_telegram_markdown


def test_markdown_preservation():
"""Test that markdown formatting is preserved correctly."""

# Create a formatter instance
formatter = MessageFormatter()
def format_response(text: str) -> str:
return preserve_telegram_markdown(text)

# Test cases: typical Letta/Broca responses
test_cases = [
Expand Down Expand Up @@ -74,7 +74,7 @@ def hello():
print("-" * 30)

# Format the response using the new method
formatted = formatter.format_response(test_case["input"])
formatted = format_response(test_case["input"])

print("Input:")
print(test_case["input"])
Expand Down Expand Up @@ -111,6 +111,8 @@ def hello():
def test_old_vs_new_behavior():
"""Compare old sanitize_text behavior vs new preserve_markdown behavior."""

from runtime.core.message import MessageFormatter

formatter = MessageFormatter()

test_input = """# Header
Expand All @@ -136,7 +138,7 @@ def test_old_vs_new_behavior():
print("\n" + "-" * 30)

# New behavior (what should happen now)
new_result = formatter.format_response(test_input)
new_result = preserve_telegram_markdown(test_input)
print("NEW behavior (format_response):")
print(new_result)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ async def test_process_outgoing_message_success(self):
) as mock_update:
await handler.process_outgoing_message(mock_message, "Test response")

mock_message.answer.assert_called_once_with("Test response")
mock_message.answer.assert_called_once()
args, kwargs = mock_message.answer.call_args
assert args == ("Test response",)
assert kwargs.get("parse_mode") == "Markdown"
mock_update.assert_called_once_with(mock_message, "sent")

@pytest.mark.asyncio
Expand Down
Loading