# 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"{name}", 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)