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