155 lines
5.7 KiB
Python
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)
|