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

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