first commit
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user