Skip to content
This repository was archived by the owner on Apr 9, 2026. It is now read-only.

Commit 4d3e764

Browse files
committed
feat: markdown-to-HTML formatting + response cleanup
- Convert OLMo markdown to Telegram HTML (bold, italic, links, code, headings) - Fallback to plain text if HTML parsing fails - Truncate hallucinated multi-turn artifacts from responses
1 parent 03f0c3c commit 4d3e764

2 files changed

Lines changed: 87 additions & 18 deletions

File tree

bot.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
VISION_MODELS,
3434
WEB2API_URL,
3535
)
36+
from formatting import md_to_telegram_html
3637
from pointing import draw_points_on_image, has_points, parse_points, strip_points
3738

3839
logging.basicConfig(
@@ -53,6 +54,19 @@
5354
# Typing indicator
5455
# ---------------------------------------------------------------------------
5556

57+
async def send_formatted(msg, text: str) -> None:
58+
"""Send a message with markdown→HTML conversion, falling back to plain text."""
59+
formatted = md_to_telegram_html(text)
60+
chunks = [formatted[i:i + 4096] for i in range(0, len(formatted), 4096)]
61+
for chunk in chunks:
62+
try:
63+
await msg.reply_text(chunk, parse_mode=ParseMode.HTML, disable_web_page_preview=True)
64+
except Exception:
65+
# If HTML parsing fails, send as plain text
66+
plain = text[chunks.index(chunk) * 4096:(chunks.index(chunk) + 1) * 4096] if len(chunks) > 1 else text
67+
await msg.reply_text(plain[:4096])
68+
69+
5670
@asynccontextmanager
5771
async def keep_typing(chat):
5872
"""Send typing indicator every 4 seconds until the block exits."""
@@ -153,6 +167,15 @@ async def query_model(
153167

154168
fields = items[0].get("fields", {})
155169
answer = fields.get("response") or fields.get("answer") or fields.get("text") or str(fields)
170+
171+
# OLMo sometimes generates fake follow-up conversations — truncate at first
172+
# occurrence of a role marker that indicates hallucinated multi-turn output.
173+
for marker in ("\nuser\n", "\nassistant\n", "\n<function_calls>"):
174+
idx = answer.find(marker)
175+
if idx > 0:
176+
answer = answer[:idx].rstrip()
177+
break
178+
156179
return answer
157180

158181

@@ -362,11 +385,7 @@ async def handle_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
362385
pointed_path = None
363386

364387
if not pointed_path:
365-
if len(answer) <= 4096:
366-
await msg.reply_text(answer)
367-
else:
368-
for i in range(0, len(answer), 4096):
369-
await msg.reply_text(answer[i:i + 4096])
388+
await send_formatted(msg, answer)
370389

371390
except httpx.ReadTimeout:
372391
await msg.reply_text("⏳ Request timed out. Vision analysis can be slow — try again.")
@@ -408,11 +427,7 @@ async def cmd_search(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None
408427
if len(user_history[uid]) > MAX_HISTORY * 2:
409428
user_history[uid] = user_history[uid][-(MAX_HISTORY * 2):]
410429

411-
if len(answer) <= 4096:
412-
await update.message.reply_text(answer)
413-
else:
414-
for i in range(0, len(answer), 4096):
415-
await update.message.reply_text(answer[i:i + 4096])
430+
await send_formatted(update.message, answer)
416431

417432
except httpx.HTTPStatusError as e:
418433
logger.error("HTTP error: %s", e)
@@ -449,14 +464,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
449464
if len(user_history[uid]) > MAX_HISTORY * 2:
450465
user_history[uid] = user_history[uid][-(MAX_HISTORY * 2):]
451466

452-
# Telegram has a 4096 char limit
453-
if len(answer) <= 4096:
454-
await update.message.reply_text(answer)
455-
else:
456-
# Split into chunks
457-
for i in range(0, len(answer), 4096):
458-
chunk = answer[i:i + 4096]
459-
await update.message.reply_text(chunk)
467+
await send_formatted(update.message, answer)
460468

461469
except httpx.HTTPStatusError as e:
462470
logger.error("HTTP error: %s", e)

formatting.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Convert markdown to Telegram-safe HTML."""
2+
3+
from __future__ import annotations
4+
5+
import html
6+
import re
7+
8+
9+
def md_to_telegram_html(text: str) -> str:
10+
"""Convert common markdown to Telegram HTML.
11+
12+
Handles: **bold**, *italic*, `inline code`, ```code blocks```,
13+
[text](url), and # headings. Everything else is HTML-escaped.
14+
"""
15+
# Extract code blocks first to protect their content
16+
code_blocks: list[str] = []
17+
18+
def _save_code_block(m: re.Match) -> str:
19+
lang = m.group(1) or ""
20+
code = html.escape(m.group(2))
21+
code_blocks.append(f"<pre>{code}</pre>")
22+
return f"\x00CODEBLOCK{len(code_blocks) - 1}\x00"
23+
24+
text = re.sub(r"```(\w*)\n?(.*?)```", _save_code_block, text, flags=re.DOTALL)
25+
26+
# Extract inline code
27+
inline_codes: list[str] = []
28+
29+
def _save_inline_code(m: re.Match) -> str:
30+
code = html.escape(m.group(1))
31+
inline_codes.append(f"<code>{code}</code>")
32+
return f"\x00INLINE{len(inline_codes) - 1}\x00"
33+
34+
text = re.sub(r"`([^`]+)`", _save_inline_code, text)
35+
36+
# HTML-escape the rest
37+
text = html.escape(text)
38+
39+
# Links: [text](url)
40+
text = re.sub(
41+
r"\[([^\]]+)\]\(([^)]+)\)",
42+
r'<a href="\2">\1</a>',
43+
text,
44+
)
45+
46+
# Bold: **text**
47+
text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
48+
49+
# Italic: *text* (but not inside words like file*name)
50+
text = re.sub(r"(?<!\w)\*(.+?)\*(?!\w)", r"<i>\1</i>", text)
51+
52+
# Headings: # Title → bold
53+
text = re.sub(r"^#{1,6}\s+(.+)$", r"<b>\1</b>", text, flags=re.MULTILINE)
54+
55+
# Restore code blocks and inline code
56+
for i, block in enumerate(code_blocks):
57+
text = text.replace(f"\x00CODEBLOCK{i}\x00", block)
58+
for i, code in enumerate(inline_codes):
59+
text = text.replace(f"\x00INLINE{i}\x00", code)
60+
61+
return text

0 commit comments

Comments
 (0)