Files
GorichBot/bot/handlers/client/main.py
T
2026-05-12 23:37:04 +03:00

155 lines
5.7 KiB
Python

# Aiogram
import aiogram.types as types
from aiogram import Router, F
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import BufferedInputFile
import aiohttp
# Keyboards
from keyboards.reply_keyboards import get_client_main_kb
# Services
from services.rag_api import ask_rag_api, RagApiError
# States
from states.client_states import MainStates
# Utils
from utils.text_tools import format_telegram_html
client_main_router = Router()
POPULAR_QUESTION_MAP = {
"🕒 До скольки вы работаете?": "До скольки вы работаете?",
"🚚 Как работает доставка?": "Есть ли доставка и как заказать?",
"🌯 Что посоветуете из шаурмы?": "Что посоветуете из шаурмы?",
"🍕 Подобрать пиццу до 400 ₽": "Подбери вкусную пиццу до 400 рублей",
"🧀 Что у вас есть с сыром?": "Что у вас есть с сыром?",
"🔥 Что у вас есть острое?": "Что у вас есть острое?",
}
MAX_HISTORY_MESSAGES = 8
MAX_MENU_CARDS = 3
PHOTO_DOWNLOAD_TIMEOUT_SECONDS = 20
def trim_history(history: list[dict[str, str]]) -> list[dict[str, str]]:
return history[-MAX_HISTORY_MESSAGES:]
def shorten_text(text: str, limit: int = 240) -> str:
cleaned = " ".join(str(text).split())
if len(cleaned) <= limit:
return cleaned
return cleaned[: limit - 1].rstrip() + ""
def build_menu_item_caption(item: dict[str, str]) -> str:
name = format_telegram_html(item.get("name", "Позиция из меню"))
raw_price = item.get("price_label") or "Цена уточняется"
if item.get("price") is None or "бесплат" in str(raw_price).lower():
raw_price = "Цена уточняется"
price = format_telegram_html(raw_price)
description = format_telegram_html(shorten_text(item.get("description", "")))
size = format_telegram_html(item.get("size") or "")
category = format_telegram_html(item.get("category") or "")
caption_parts = [f"<b>{name}</b>", f"💸 {price}"]
if category or size:
meta = "".join(part for part in [category, size] if part)
caption_parts.append(meta)
if description:
caption_parts.append(description)
return "\n".join(caption_parts)
async def send_menu_cards(message: types.Message, items: list[dict[str, str]]) -> None:
for item in items[:MAX_MENU_CARDS]:
caption = build_menu_item_caption(item)
photo_url = item.get("photo_url")
if photo_url:
try:
photo = await download_menu_photo(str(photo_url), str(item.get("item_id") or "menu"))
await message.answer_photo(photo=photo, caption=caption)
continue
except TelegramBadRequest:
pass
except Exception:
try:
await message.answer_photo(photo=photo_url, caption=caption)
continue
except TelegramBadRequest:
pass
await message.answer(caption)
async def download_menu_photo(photo_url: str, item_id: str) -> BufferedInputFile:
timeout = aiohttp.ClientTimeout(total=PHOTO_DOWNLOAD_TIMEOUT_SECONDS)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(photo_url) as response:
response.raise_for_status()
content = await response.read()
extension = photo_url.rsplit(".", 1)[-1].split("?", 1)[0] if "." in photo_url else "jpg"
filename = f"menu_{item_id}.{extension or 'jpg'}"
return BufferedInputFile(content, filename=filename)
@client_main_router.message(F.text == "🧹 Очистить диалог", StateFilter(MainStates.main))
async def clear_dialog(message: types.Message, state: FSMContext):
await state.update_data(rag_history=[])
await message.answer(
"🧼 Диалог очищен. Можете задать новый вопрос о меню, доставке или заведении.",
reply_markup=get_client_main_kb(),
)
@client_main_router.message(F.text, StateFilter(MainStates.main))
async def handle_client_message(message: types.Message, state: FSMContext):
if message.chat.type != "private":
return
if not message.text:
return
user_text = POPULAR_QUESTION_MAP.get(message.text, message.text)
state_data = await state.get_data()
history = state_data.get("rag_history", [])
waiting_message = await message.answer("🤖 Думаю над ответом...")
try:
response = await ask_rag_api(message=user_text, history=history)
except RagApiError:
await waiting_message.edit_text(
"⚠️ Не получилось обратиться к сервису ответов. Попробуйте ещё раз через минуту."
)
return
except Exception:
await waiting_message.edit_text(
"⚠️ Что-то пошло не так. Попробуйте отправить вопрос ещё раз."
)
return
answer = format_telegram_html(response.get("answer", "⚠️ Не удалось получить ответ."))
updated_history = trim_history(
[
*history,
{"role": "user", "content": user_text},
{"role": "assistant", "content": answer},
]
)
await state.update_data(rag_history=updated_history)
await waiting_message.edit_text(answer)
tool_results = response.get("tool_results") or []
if tool_results:
await send_menu_cards(message, tool_results)