Добавлено экранирование 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 import json
from io import BytesIO from io import BytesIO
from typing import Dict, List, Any, Optional from typing import Dict, List, Any, Optional
import re
from aiogram import Bot, Dispatcher, types, F from aiogram import Bot, Dispatcher, types, F
from aiogram.filters import Command from aiogram.filters import Command
@ -11,6 +12,7 @@ from aiogram.types import Message
from dotenv import load_dotenv from dotenv import load_dotenv
from ollama import AsyncClient from ollama import AsyncClient
import asyncpg import asyncpg
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
# Настройка логирования # Настройка логирования
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -151,6 +153,50 @@ async def keep_typing(chat_id: int):
logging.error(f"Ошибка в keep_typing: {e}") 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")) @dp.message(Command("start"))
async def cmd_start(message: Message): async def cmd_start(message: Message):
"""Обработчик команды /start""" """Обработчик команды /start"""
@ -165,7 +211,22 @@ async def cmd_start(message: Message):
# Если есть, сохраняем их в кэше # Если есть, сохраняем их в кэше
user_messages[user_id] = messages 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")) @dp.message(Command("clear"))
@ -245,8 +306,14 @@ async def handle_photo(message: Message):
# Сохраняем ответ в БД # Сохраняем ответ в БД
await save_message_to_db(user_id, assistant_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: except Exception as e:
# Останавливаем задачу обновления статуса печати в случае ошибки # Останавливаем задачу обновления статуса печати в случае ошибки
@ -305,8 +372,14 @@ async def handle_text(message: Message):
# Сохраняем ответ в БД # Сохраняем ответ в БД
await save_message_to_db(user_id, assistant_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: except Exception as e:
# Останавливаем задачу обновления статуса печати в случае ошибки # Останавливаем задачу обновления статуса печати в случае ошибки
@ -324,6 +397,25 @@ async def handle_other(message: Message):
await message.answer("Я могу обрабатывать только текст и фотографии. Пожалуйста, отправьте текст или одну фотографию.") 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(): async def main():
"""Запуск бота""" """Запуск бота"""
# Инициализируем подключение к базе данных # Инициализируем подключение к базе данных