121 lines
3.7 KiB
Python
121 lines
3.7 KiB
Python
import html
|
|
import re
|
|
|
|
|
|
def to_html(obj):
|
|
|
|
return html.escape(str(obj))
|
|
|
|
|
|
def format_llm_answer_html(text: str | None) -> str:
|
|
if text is None:
|
|
return ""
|
|
|
|
escaped = html.escape(str(text).replace("\r\n", "\n").strip())
|
|
normalized_lines = []
|
|
|
|
for line in escaped.split("\n"):
|
|
normalized_line = re.sub(r"^\s*[-*]\s+", "• ", line.rstrip())
|
|
normalized_lines.append(normalized_line)
|
|
|
|
formatted = "\n".join(normalized_lines)
|
|
formatted = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", formatted)
|
|
return formatted
|
|
|
|
|
|
def split_plain_text_chunks(text: str | None, limit: int = 3500) -> list[str]:
|
|
if text is None:
|
|
return [""]
|
|
|
|
normalized = str(text).replace("\r\n", "\n").strip()
|
|
if not normalized:
|
|
return [""]
|
|
|
|
paragraphs = normalized.split("\n\n")
|
|
chunks: list[str] = []
|
|
current = ""
|
|
|
|
for paragraph in paragraphs:
|
|
paragraph = paragraph.strip()
|
|
if not paragraph:
|
|
continue
|
|
|
|
candidate = paragraph if not current else f"{current}\n\n{paragraph}"
|
|
if len(candidate) <= limit:
|
|
current = candidate
|
|
continue
|
|
|
|
if current:
|
|
chunks.append(current)
|
|
current = ""
|
|
|
|
if len(paragraph) <= limit:
|
|
current = paragraph
|
|
continue
|
|
|
|
lines = paragraph.split("\n")
|
|
line_buffer = ""
|
|
|
|
for line in lines:
|
|
line = line.rstrip()
|
|
line_candidate = line if not line_buffer else f"{line_buffer}\n{line}"
|
|
if len(line_candidate) <= limit:
|
|
line_buffer = line_candidate
|
|
continue
|
|
|
|
if line_buffer:
|
|
chunks.append(line_buffer)
|
|
line_buffer = ""
|
|
|
|
while len(line) > limit:
|
|
chunks.append(line[:limit].rstrip())
|
|
line = line[limit:].lstrip()
|
|
|
|
line_buffer = line
|
|
|
|
if line_buffer:
|
|
current = line_buffer
|
|
|
|
if current:
|
|
chunks.append(current)
|
|
|
|
return chunks or [normalized[:limit]]
|
|
|
|
|
|
def parse_links_to_inline_markup(message: str) -> list:
|
|
"""
|
|
Парсит сообщение с форматированными ссылками и возвращает список рядов кнопок.
|
|
|
|
Формат входного сообщения:
|
|
- [Текст кнопки + Ссылка] для одной кнопки.
|
|
- [Кнопка1 + Ссылка1][Кнопка2 + Ссылка2] для нескольких кнопок в одном ряду.
|
|
- Каждая строка представляет отдельный ряд кнопок.
|
|
|
|
Пример:
|
|
[Кнопка1 + https://example.com]
|
|
[Кнопка2 + https://example.org][Кнопка3 + https://example.net]
|
|
|
|
:param message: Строка с отформатированными ссылками.
|
|
:return: Список рядов кнопок, где каждый ряд — это список кортежей (Текст, Ссылка).
|
|
"""
|
|
# Исправленное регулярное выражение для поиска [Текст + Ссылка]
|
|
pattern = re.compile(r"\[([^\[\]+]+)\s*\+\s*(https?://[^\[\]]+)\]")
|
|
|
|
# Инициализируем список рядов кнопок
|
|
keyboard_rows = []
|
|
|
|
# Разбиваем сообщение на строки
|
|
lines = message.strip().split("\n")
|
|
|
|
for line in lines:
|
|
# Находим все совпадения в строке
|
|
matches = pattern.findall(line)
|
|
if matches:
|
|
row = []
|
|
for text, url in matches:
|
|
button = (text.strip(), url.strip())
|
|
row.append(button)
|
|
keyboard_rows.append(row)
|
|
|
|
return keyboard_rows
|