Добавлены функции для разделения и отправки длинных сообщений, а также обработка ошибок при отправке с форматированием. Введен начальный промт для ИИ и обновлено управление контекстом диалога, включая сохранение начального промта в БД.
This commit is contained in:
parent
a3fd7baa6a
commit
7adc139cdf
119
bot.py
119
bot.py
@ -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("Контекст диалога очищен!")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user