Добавлено экранирование Markdown в сообщениях бота и обработчик кнопки для очистки истории диалога. Обновлено приветственное сообщение с инструкциями по форматированию.

This commit is contained in:
NikitolProject 2025-04-27 06:54:33 +03:00
parent d2af1e23de
commit c9feb61ceb

102
bot.py
View File

@ -4,6 +4,7 @@ import logging
import json
from io import BytesIO
from typing import Dict, List, Any, Optional
import re
from aiogram import Bot, Dispatcher, types, F
from aiogram.filters import Command
@ -11,6 +12,7 @@ from aiogram.types import Message
from dotenv import load_dotenv
from ollama import AsyncClient
import asyncpg
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
# Настройка логирования
logging.basicConfig(level=logging.INFO)
@ -151,6 +153,50 @@ async def keep_typing(chat_id: int):
logging.error(f"Ошибка в keep_typing: {e}")
def escape_markdown(text: str) -> str:
"""Экранирует специальные символы Markdown, если они не являются частью форматирования"""
# Регулярное выражение для поиска уже отформатированного текста
md_patterns = [
r'`[^`]+`', # Код
r'\*\*[^*]+\*\*', # Жирный
r'\*[^*]+\*', # Курсив
r'_[^_]+_', # Курсив (альтернативный)
r'__[^_]+__', # Жирный (альтернативный)
r'\[[^\]]+\]\([^)]+\)', # Ссылки
]
# Собираем все совпадения с шаблонами
matches = []
for pattern in md_patterns:
matches.extend([(m.start(), m.end()) for m in re.finditer(pattern, text)])
# Сортируем по началу совпадения
matches.sort()
# Если нет совпадений, просто экранируем все специальные символы
if not matches:
return re.sub(r'([_*\[\]()~`>#+-=|{}.!\\])', r'\\\1', text)
# Иначе экранируем только те символы, которые не входят в уже отформатированный текст
result = ""
last_pos = 0
for start, end in matches:
# Экранируем символы до начала форматирования
if start > last_pos:
result += re.sub(r'([_*\[\]()~`>#+-=|{}.!\\])', r'\\\1', text[last_pos:start])
# Добавляем отформатированный текст без изменений
result += text[start:end]
last_pos = end
# Экранируем символы после последнего форматирования
if last_pos < len(text):
result += re.sub(r'([_*\[\]()~`>#+-=|{}.!\\])', r'\\\1', text[last_pos:])
return result
@dp.message(Command("start"))
async def cmd_start(message: Message):
"""Обработчик команды /start"""
@ -165,7 +211,22 @@ async def cmd_start(message: Message):
# Если есть, сохраняем их в кэше
user_messages[user_id] = messages
await message.answer("Привет! Я бот на базе Gemma. Отправьте мне сообщение или фото, и я отвечу вам.")
welcome_message = (
"Привет! Я бот на базе Gemma. Отправьте мне сообщение или фото, и я отвечу вам.\n\n"
"Поддерживается *Markdown* форматирование:\n"
"• *жирный текст* (звездочки)\n"
"• _курсив_ (нижнее подчеркивание)\n"
"• `код` (обратные кавычки)\n"
"• [ссылки](https://example.com)\n\n"
"Используйте /clear для очистки контекста диалога."
)
# Создаем инлайн-кнопку для очистки истории
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🗑 Очистить историю сообщений", callback_data="clear_history")]
])
await message.answer(welcome_message, parse_mode=types.ParseMode.MARKDOWN, reply_markup=keyboard)
@dp.message(Command("clear"))
@ -245,8 +306,14 @@ async def handle_photo(message: Message):
# Сохраняем ответ в БД
await save_message_to_db(user_id, assistant_message)
# Отправляем ответ пользователю
await message.answer(answer)
# Создаем инлайн-кнопку для очистки истории
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🗑 Очистить историю сообщений", callback_data="clear_history")]
])
# Отправляем ответ пользователю с Markdown-форматированием и кнопкой
safe_answer = escape_markdown(answer)
await message.answer(safe_answer, parse_mode=types.ParseMode.MARKDOWN, reply_markup=keyboard)
except Exception as e:
# Останавливаем задачу обновления статуса печати в случае ошибки
@ -305,8 +372,14 @@ async def handle_text(message: Message):
# Сохраняем ответ в БД
await save_message_to_db(user_id, assistant_message)
# Отправляем ответ пользователю
await message.answer(answer)
# Создаем инлайн-кнопку для очистки истории
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🗑 Очистить историю сообщений", callback_data="clear_history")]
])
# Отправляем ответ пользователю с Markdown-форматированием и кнопкой
safe_answer = escape_markdown(answer)
await message.answer(safe_answer, parse_mode=types.ParseMode.MARKDOWN, reply_markup=keyboard)
except Exception as e:
# Останавливаем задачу обновления статуса печати в случае ошибки
@ -324,6 +397,25 @@ async def handle_other(message: Message):
await message.answer("Я могу обрабатывать только текст и фотографии. Пожалуйста, отправьте текст или одну фотографию.")
# Добавляем обработчик нажатия на кнопку очистки истории
@dp.callback_query(F.data == "clear_history")
async def clear_history_callback(callback: types.CallbackQuery):
"""Обработчик кнопки очистки истории"""
user_id = callback.from_user.id
# Очищаем контекст в памяти
user_messages[user_id] = []
# Очищаем контекст в БД
await clear_messages_from_db(user_id)
# Уведомляем пользователя
await callback.answer("Контекст диалога очищен!")
# Отправляем сообщение в чат
await callback.message.answer("Контекст диалога очищен. Вы можете начать новый разговор.")
async def main():
"""Запуск бота"""
# Инициализируем подключение к базе данных