Files
hermes-agent/plugins/platforms/telegram/telegram_entities.py
T
teknium1 d7ceee19a8 refactor(telegram): move text_link expansion to telegram_entities sibling
The adapter facade is ~6.6k lines; new behaviour belongs in a topical sibling per the
facade+siblings layout. expand_link_entities() now lives in telegram_entities.py and
reuses the encode/decode UTF-16 slicing the adapter already uses for entity spans.

Also: skip inlining when the anchor text already is the URL (no 'url (url)' duplication),
trim the test file to the invariants and point it at the sibling.
2026-09-13 20:58:39 -07:00

69 lines
2.6 KiB
Python

"""Telegram ``text_link`` expansion, kept out of the adapter facade.
A rich-text hyperlink arrives as visible anchor text plus a ``text_link`` entity carrying the
URL; the model only ever sees the anchor unless the URL is inlined (#31071).
"""
from __future__ import annotations
from typing import Any
def _utf16_length(text: str) -> int:
return len(text.encode("utf-16-le")) // 2
def _code_point_index(text: str, utf16_offset: int) -> int | None:
"""Python index for a UTF-16 code-unit offset; None when it splits a surrogate pair."""
try:
return len(text.encode("utf-16-le")[: utf16_offset * 2].decode("utf-16-le"))
except UnicodeDecodeError:
return None
def expand_link_entities(message: Any) -> str:
"""Message text (or caption) with every hidden ``text_link`` URL inlined after its anchor.
Entity offsets are UTF-16 code units (emoji before the anchor count twice), so they are mapped
to code-point indices before slicing. Malformed entities are skipped; an anchor that already
reads as its own URL, or text already carrying the inline form, is left untouched.
"""
text = getattr(message, "text", None)
if text:
entities = getattr(message, "entities", None) or []
else:
text = getattr(message, "caption", None) or ""
entities = getattr(message, "caption_entities", None) or []
if not text or not entities:
return text
utf16_length = _utf16_length(text)
links: list[tuple[int, int, str]] = []
for entity in entities:
entity_type = str(getattr(entity, "type", "")).split(".")[-1].lower()
raw_url = getattr(entity, "url", None)
url = raw_url.strip() if isinstance(raw_url, str) else ""
if entity_type != "text_link" or not url:
continue
try:
offset = int(getattr(entity, "offset", -1))
length = int(getattr(entity, "length", 0))
except (TypeError, ValueError):
continue
if offset < 0 or length <= 0 or offset + length > utf16_length:
continue
start, end = _code_point_index(text, offset), _code_point_index(text, offset + length)
if start is None or end is None or end <= start:
continue
if text[start:end].strip() == url:
continue
links.append((start, end, url))
expanded = text
for _start, end, url in sorted(links, reverse=True):
inline = f" ({url})"
if expanded[end:].startswith(inline):
continue
expanded = f"{expanded[:end]}{inline}{expanded[end:]}"
return expanded