Добавлены функции для разделения и отправки длинных сообщений, а также обработка ошибок при отправке с форматированием. Введен начальный промт для ИИ и обновлено управление контекстом диалога, включая сохранение начального промта в БД.

This commit is contained in:
NikitolProject 2025-04-27 15:37:42 +03:00
parent a3fd7baa6a
commit 7adc139cdf

119
bot.py
View File

@ -5,6 +5,7 @@ import json
from io import BytesIO
from typing import Dict, List, Any, Optional
import re
import textwrap
from aiogram import Bot, Dispatcher, types, F
from aiogram.filters import Command
@ -37,6 +38,15 @@ user_messages: Dict[int, List[Dict[str, Any]]] = {}
# Словарь для хранения задач обновления статуса печати
typing_tasks: Dict[int, asyncio.Task] = {}
# Максимальная длина сообщения Telegram
MAX_MESSAGE_LENGTH = 4096
# Задержка между отправкой разделенных сообщений
SPLIT_MESSAGE_DELAY = 0.5
# Начальный промт для ИИ
INITIAL_PROMPT = "Ты - Альфа, виртуальный помощник, который должен выполнять любые запросы от пользователя. Взаимодействие с тобой происходит через мессенджер Telegram, поэтому тебе важно писать текст с форматированием, допустимым для этого мессенджера (в Markdown)"
async def init_db():
"""Инициализация базы данных"""
@ -159,6 +169,51 @@ def escape_markdown(text: str) -> str:
return text
async def split_and_send_message(message: Message, text: str, parse_mode=None, reply_markup=None):
"""Функция для разделения и отправки длинных сообщений"""
# Если сообщение меньше максимальной длины, отправляем как есть
if len(text) <= MAX_MESSAGE_LENGTH:
return await message.answer(text, parse_mode=parse_mode, reply_markup=reply_markup)
# Иначе разделяем сообщение
chunks = textwrap.wrap(text, MAX_MESSAGE_LENGTH, replace_whitespace=False, drop_whitespace=False)
# Отправляем первый кусок с клавиатурой (если есть)
await message.answer(chunks[0], parse_mode=parse_mode)
# Отправляем остальные куски с задержкой
for chunk in chunks[1:]:
await asyncio.sleep(SPLIT_MESSAGE_DELAY)
await message.answer(chunk, parse_mode=parse_mode)
# Отправляем последнее сообщение с клавиатурой (если есть)
if reply_markup and len(chunks) > 1:
await message.answer("⬆️ Полный ответ ⬆️", reply_markup=reply_markup)
return None
async def handle_message_with_retry(message: Message, text: str, keyboard=None):
"""Функция для отправки сообщения с повторной попыткой без форматирования при ошибке"""
try:
# Пытаемся отправить сообщение с форматированием Markdown
await split_and_send_message(message, text, parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard)
except Exception as e:
error_text = str(e)
logging.error(f"Ошибка при отправке сообщения с форматированием: {error_text}")
# Проверяем, связана ли ошибка с разбором сущностей (ошибка форматирования)
if "can't parse entities" in error_text or "entities" in error_text:
# Отправляем сообщение о проблеме с форматированием
await message.answer("⚠️ Возникла проблема с форматированием сообщения. Отправляю без форматирования.")
# Отправляем сообщение без форматирования
await split_and_send_message(message, text, reply_markup=keyboard)
else:
# Если ошибка не связана с форматированием, просто пробрасываем её дальше
raise e
@dp.message(Command("start"))
async def cmd_start(message: Message):
"""Обработчик команды /start"""
@ -167,8 +222,8 @@ async def cmd_start(message: Message):
# Загружаем контекст из БД при первом запуске
messages = await load_messages_from_db(user_id)
if not messages:
# Если сообщений нет, создаем новый список
user_messages[user_id] = []
# Если сообщений нет, создаем новый список с начальным промтом
user_messages[user_id] = [{"role": "system", "content": INITIAL_PROMPT}]
else:
# Если есть, сохраняем их в кэше
user_messages[user_id] = messages
@ -196,12 +251,15 @@ async def cmd_clear(message: Message):
"""Очистка контекста диалога"""
user_id = message.from_user.id
# Очищаем контекст в памяти
user_messages[user_id] = []
# Очищаем контекст в памяти и добавляем начальный промт
user_messages[user_id] = [{"role": "system", "content": INITIAL_PROMPT}]
# Очищаем контекст в БД
await clear_messages_from_db(user_id)
# Сохраняем начальный промт в БД
await save_message_to_db(user_id, user_messages[user_id][0])
await message.answer("Контекст диалога очищен.")
@ -214,7 +272,19 @@ async def handle_photo(message: Message):
if user_id not in user_messages:
# Пытаемся загрузить контекст из БД
messages = await load_messages_from_db(user_id)
user_messages[user_id] = messages or []
if not messages:
# Если нет сообщений, создаем начальный промт
user_messages[user_id] = [{"role": "system", "content": INITIAL_PROMPT}]
# Сохраняем начальный промт в БД
await save_message_to_db(user_id, user_messages[user_id][0])
else:
# Проверяем, есть ли начальный промт
if not messages or messages[0].get("role") != "system":
# Добавляем начальный промт в начало истории
messages.insert(0, {"role": "system", "content": INITIAL_PROMPT})
# Сохраняем начальный промт в БД
await save_message_to_db(user_id, messages[0])
user_messages[user_id] = messages
# Получаем фото наилучшего качества
photo = message.photo[-1]
@ -247,12 +317,17 @@ async def handle_photo(message: Message):
try:
# Получаем ответ от Gemma
answer = ""
print("AI stream: ", end="", flush=True)
async for part in await ollama_client.chat(
model=model_name,
messages=user_messages[user_id],
stream=True
):
answer += part['message']['content']
content = part['message']['content']
answer += content
# Логируем поток от ИИ без переноса строки и с flush=True
print(content, end="", flush=True)
# Останавливаем задачу обновления статуса печати
if user_id in typing_tasks:
@ -275,7 +350,7 @@ async def handle_photo(message: Message):
# Отправляем ответ пользователю с Markdown-форматированием и кнопкой
safe_answer = escape_markdown(answer)
await message.answer(safe_answer, parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard)
await handle_message_with_retry(message, safe_answer, keyboard)
except Exception as e:
# Останавливаем задачу обновления статуса печати в случае ошибки
@ -296,7 +371,19 @@ async def handle_text(message: Message):
if user_id not in user_messages:
# Пытаемся загрузить контекст из БД
messages = await load_messages_from_db(user_id)
user_messages[user_id] = messages or []
if not messages:
# Если нет сообщений, создаем начальный промт
user_messages[user_id] = [{"role": "system", "content": INITIAL_PROMPT}]
# Сохраняем начальный промт в БД
await save_message_to_db(user_id, user_messages[user_id][0])
else:
# Проверяем, есть ли начальный промт
if not messages or messages[0].get("role") != "system":
# Добавляем начальный промт в начало истории
messages.insert(0, {"role": "system", "content": INITIAL_PROMPT})
# Сохраняем начальный промт в БД
await save_message_to_db(user_id, messages[0])
user_messages[user_id] = messages
# Создаем сообщение пользователя
user_message = {"role": "user", "content": message.text}
@ -313,12 +400,17 @@ async def handle_text(message: Message):
try:
# Получаем ответ от Gemma
answer = ""
print("AI stream: ", end="", flush=True)
async for part in await ollama_client.chat(
model=model_name,
messages=user_messages[user_id],
stream=True
):
answer += part['message']['content']
content = part['message']['content']
answer += content
# Логируем поток от ИИ без переноса строки и с flush=True
print(content, end="", flush=True)
# Останавливаем задачу обновления статуса печати
if user_id in typing_tasks:
@ -341,7 +433,7 @@ async def handle_text(message: Message):
# Отправляем ответ пользователю с Markdown-форматированием и кнопкой
safe_answer = escape_markdown(answer)
await message.answer(safe_answer, parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard)
await handle_message_with_retry(message, safe_answer, keyboard)
except Exception as e:
# Останавливаем задачу обновления статуса печати в случае ошибки
@ -365,12 +457,15 @@ async def clear_history_callback(callback: types.CallbackQuery):
"""Обработчик кнопки очистки истории"""
user_id = callback.from_user.id
# Очищаем контекст в памяти
user_messages[user_id] = []
# Очищаем контекст в памяти и добавляем начальный промт
user_messages[user_id] = [{"role": "system", "content": INITIAL_PROMPT}]
# Очищаем контекст в БД
await clear_messages_from_db(user_id)
# Сохраняем начальный промт в БД
await save_message_to_db(user_id, user_messages[user_id][0])
# Уведомляем пользователя
await callback.answer("Контекст диалога очищен!")