From c9feb61ceb31cfb4b3a3ba396c1cf36214b83155 Mon Sep 17 00:00:00 2001 From: NikitolProject Date: Sun, 27 Apr 2025 06:54:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20Markdown=20=D0=B2=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=D1=85=20=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=20=D0=B8=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D0=B8=D0=BA=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=87=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0.=20=D0=9E=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D1=81=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=BF=D0=BE=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index c4a3848..486d5b2 100644 --- a/bot.py +++ b/bot.py @@ -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(): """Запуск бота""" # Инициализируем подключение к базе данных