Бот-ассистент закрывает три задачи: отвечает на частые вопросы, записывает клиентов на услуги и показывает статус заказа. Без менеджера, 24/7. Разберём проектирование диалогов, FSM для записи, интеграцию с CRM и авто-ответы на FAQ.
Архитектура бота
assistant_bot/
├── __main__.py
├── config.py
├── handlers/
│ ├── __init__.py
│ ├── start.py
│ ├── faq.py # авто-ответы
│ ├── booking.py # запись на услугу
│ └── order_status.py # проверка статуса
├── keyboards/
│ ├── main_menu.py
│ └── booking.py
├── states/
│ └── booking.py
├── services/
│ ├── faq_service.py # поиск по базе FAQ
│ └── crm.py # интеграция с CRM
└── data/
└── faq.json # база вопросов-ответов
Главное меню
Пользователь нажимает /start — видит три кнопки. Каждая ведёт в свой сценарий:
# keyboards/main_menu.py
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
def main_menu_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="Частые вопросы", callback_data="menu:faq"
)],
[InlineKeyboardButton(
text="Записаться на услугу", callback_data="menu:booking"
)],
[InlineKeyboardButton(
text="Статус заказа", callback_data="menu:order_status"
)],
[InlineKeyboardButton(
text="Связаться с менеджером", url="https://t.me/manager"
)],
])
# handlers/start.py
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message, CallbackQuery
from keyboards.main_menu import main_menu_kb
router = Router()
@router.message(CommandStart())
async def cmd_start(message: Message):
await message.answer(
f"Здравствуйте, {message.from_user.first_name}!\n\n"
"Я помогу ответить на вопросы, записать на услугу "
"или проверить статус заказа.",
reply_markup=main_menu_kb(),
)
@router.callback_query(lambda c: c.data == "menu:back")
async def back_to_menu(callback: CallbackQuery):
await callback.message.edit_text(
"Выберите действие:",
reply_markup=main_menu_kb(),
)
FAQ: авто-ответы на частые вопросы
Храним вопросы-ответы в JSON. При нажатии на категорию — показываем список вопросов. При нажатии на вопрос — отвечаем.
data/faq.json
{
"categories": [
{
"id": "delivery",
"title": "Доставка",
"questions": [
{
"id": "delivery_time",
"question": "Сколько идёт доставка?",
"answer": "Доставка по Москве — 1-2 дня, по России — 3-7 дней. После отправки вы получите трек-номер."
},
{
"id": "delivery_cost",
"question": "Сколько стоит доставка?",
"answer": "Бесплатно при заказе от 3 000 ₽. Иначе — от 300 ₽ в зависимости от региона."
}
]
},
{
"id": "payment",
"title": "Оплата",
"questions": [
{
"id": "payment_methods",
"question": "Какие способы оплаты?",
"answer": "Банковская карта, СБП, наличные при получении. Для юрлиц — оплата по счёту."
},
{
"id": "payment_installment",
"question": "Есть рассрочка?",
"answer": "Да, рассрочка 0-0-4 через Т-Банк при заказе от 5 000 ₽."
}
]
},
{
"id": "returns",
"title": "Возврат",
"questions": [
{
"id": "return_policy",
"question": "Как вернуть товар?",
"answer": "В течение 14 дней с момента получения. Товар должен быть в оригинальной упаковке, без следов использования. Напишите менеджеру для оформления возврата."
}
]
}
]
}
services/faq_service.py
import json
from pathlib import Path
class FAQService:
def __init__(self):
faq_path = Path(__file__).parent.parent / "data" / "faq.json"
with open(faq_path, encoding="utf-8") as f:
self.data = json.load(f)
def get_categories(self) -> list[dict]:
return [
{"id": c["id"], "title": c["title"]}
for c in self.data["categories"]
]
def get_questions(self, category_id: str) -> list[dict]:
for cat in self.data["categories"]:
if cat["id"] == category_id:
return cat["questions"]
return []
def get_answer(self, question_id: str) -> str | None:
for cat in self.data["categories"]:
for q in cat["questions"]:
if q["id"] == question_id:
return q["answer"]
return None
faq_service = FAQService()
handlers/faq.py
from aiogram import Router, F
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from services.faq_service import faq_service
router = Router()
@router.callback_query(F.data == "menu:faq")
async def show_faq_categories(callback: CallbackQuery):
categories = faq_service.get_categories()
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=cat["title"], callback_data=f"faq:cat:{cat['id']}"
)]
for cat in categories
] + [
[InlineKeyboardButton(text="← Назад", callback_data="menu:back")]
])
await callback.message.edit_text(
"Выберите категорию:", reply_markup=kb
)
@router.callback_query(F.data.startswith("faq:cat:"))
async def show_faq_questions(callback: CallbackQuery):
category_id = callback.data.split(":")[2]
questions = faq_service.get_questions(category_id)
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=q["question"], callback_data=f"faq:q:{q['id']}"
)]
for q in questions
] + [
[InlineKeyboardButton(text="← Назад", callback_data="menu:faq")]
])
await callback.message.edit_text(
"Выберите вопрос:", reply_markup=kb
)
@router.callback_query(F.data.startswith("faq:q:"))
async def show_faq_answer(callback: CallbackQuery):
question_id = callback.data.split(":")[2]
answer = faq_service.get_answer(question_id)
if not answer:
await callback.answer("Ответ не найден", show_alert=True)
return
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="← К вопросам", callback_data="menu:faq")],
[InlineKeyboardButton(text="← Главное меню", callback_data="menu:back")],
])
await callback.message.edit_text(answer, reply_markup=kb)
Запись на услугу (FSM)
Пошаговый сценарий: выбор услуги → дата → время → контакт → подтверждение.
states/booking.py
from aiogram.fsm.state import State, StatesGroup
class BookingState(StatesGroup):
service = State()
date = State()
time = State()
phone = State()
confirm = State()
handlers/booking.py
import re
from datetime import datetime, timedelta
from aiogram import Router, F
from aiogram.types import CallbackQuery, Message, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from states.booking import BookingState
router = Router()
SERVICES = [
{"id": "consult", "name": "Консультация", "duration": "30 мин", "price": 0},
{"id": "design", "name": "Дизайн-проект", "duration": "1 час", "price": 5000},
{"id": "develop", "name": "Разработка сайта", "duration": "1 час", "price": 3000},
]
@router.callback_query(F.data == "menu:booking")
async def start_booking(callback: CallbackQuery, state: FSMContext):
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"{s['name']} ({s['duration']}) — {s['price']} ₽" if s["price"] else f"{s['name']} ({s['duration']}) — бесплатно",
callback_data=f"book:service:{s['id']}",
)]
for s in SERVICES
] + [
[InlineKeyboardButton(text="← Назад", callback_data="menu:back")]
])
await state.set_state(BookingState.service)
await callback.message.edit_text("Выберите услугу:", reply_markup=kb)
@router.callback_query(BookingState.service, F.data.startswith("book:service:"))
async def choose_date(callback: CallbackQuery, state: FSMContext):
service_id = callback.data.split(":")[2]
service = next(s for s in SERVICES if s["id"] == service_id)
await state.update_data(service=service)
# Генерируем кнопки с датами на ближайшие 7 дней
dates = []
for i in range(1, 8):
d = datetime.now() + timedelta(days=i)
if d.weekday() < 6: # исключаем воскресенье
dates.append(d)
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=d.strftime("%d.%m (%a)").replace("Mon", "Пн").replace("Tue", "Вт").replace("Wed", "Ср").replace("Thu", "Чт").replace("Fri", "Пт").replace("Sat", "Сб"),
callback_data=f"book:date:{d.strftime('%Y-%m-%d')}",
)]
for d in dates
])
await state.set_state(BookingState.date)
await callback.message.edit_text(
f"Услуга: {service['name']}\n\nВыберите дату:",
reply_markup=kb,
)
@router.callback_query(BookingState.date, F.data.startswith("book:date:"))
async def choose_time(callback: CallbackQuery, state: FSMContext):
date_str = callback.data.split(":")[2]
await state.update_data(date=date_str)
times = ["10:00", "11:00", "12:00", "14:00", "15:00", "16:00", "17:00"]
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t, callback_data=f"book:time:{t}")]
for t in times
])
await state.set_state(BookingState.time)
await callback.message.edit_text(
f"Дата: {date_str}\n\nВыберите время:",
reply_markup=kb,
)
@router.callback_query(BookingState.time, F.data.startswith("book:time:"))
async def ask_phone(callback: CallbackQuery, state: FSMContext):
time_str = callback.data.split(":")[2]
await state.update_data(time=time_str)
await state.set_state(BookingState.phone)
await callback.message.edit_text(
"Введите номер телефона для связи:"
)
@router.message(BookingState.phone)
async def process_phone(message: Message, state: FSMContext):
phone = re.sub(r"\D", "", message.text)
if len(phone) != 11:
await message.answer("Введите 11 цифр номера:")
return
await state.update_data(phone=phone)
data = await state.get_data()
service = data["service"]
kb = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="Подтвердить", callback_data="book:confirm"),
InlineKeyboardButton(text="Отмена", callback_data="book:cancel"),
]
])
await state.set_state(BookingState.confirm)
await message.answer(
f"Проверьте запись:\n\n"
f"Услуга: {service['name']}\n"
f"Дата: {data['date']}\n"
f"Время: {data['time']}\n"
f"Телефон: {phone}\n"
f"Стоимость: {service['price']} ₽" if service["price"] else f"Стоимость: бесплатно",
reply_markup=kb,
)
@router.callback_query(BookingState.confirm, F.data == "book:confirm")
async def confirm_booking(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
# Отправляем в CRM / админу
# await crm_service.create_booking(data)
await callback.message.edit_text(
"Запись подтверждена! Мы свяжемся с вами для подтверждения.\n\n"
"Если нужно отменить или перенести — напишите менеджеру."
)
await state.clear()
@router.callback_query(BookingState.confirm, F.data == "book:cancel")
async def cancel_booking(callback: CallbackQuery, state: FSMContext):
await callback.message.edit_text("Запись отменена.")
await state.clear()
Статус заказа
Пользователь вводит номер заказа — бот запрашивает статус через API CRM:
# services/crm.py
import httpx
class CRMService:
def __init__(self, api_url: str, api_key: str):
self.api_url = api_url
self.api_key = api_key
async def get_order_status(self, order_id: str) -> dict | None:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{self.api_url}/orders/{order_id}",
headers={"Authorization": f"Bearer {self.api_key}"},
)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
# handlers/order_status.py
from aiogram import Router, F
from aiogram.types import CallbackQuery, Message, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from services.crm import CRMService
router = Router()
class OrderStatusState(StatesGroup):
waiting_order_id = State()
STATUS_LABELS = {
"new": "Новый",
"processing": "В обработке",
"shipped": "Отправлен",
"delivered": "Доставлен",
"cancelled": "Отменён",
}
@router.callback_query(F.data == "menu:order_status")
async def ask_order_id(callback: CallbackQuery, state: FSMContext):
await state.set_state(OrderStatusState.waiting_order_id)
await callback.message.edit_text(
"Введите номер заказа (например, 12345):"
)
@router.message(OrderStatusState.waiting_order_id)
async def show_order_status(message: Message, state: FSMContext, crm: CRMService):
order_id = message.text.strip()
order = await crm.get_order_status(order_id)
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="← Главное меню", callback_data="menu:back")]
])
if not order:
await message.answer(
f"Заказ #{order_id} не найден. Проверьте номер.",
reply_markup=kb,
)
await state.clear()
return
status = STATUS_LABELS.get(order["status"], order["status"])
text = (
f"Заказ #{order_id}\n\n"
f"Статус: {status}\n"
f"Сумма: {order.get('total', '—')} ₽\n"
)
if order.get("track_number"):
text += f"Трек-номер: {order['track_number']}\n" if order.get("estimated_delivery"): text += f"Ожидаемая доставка: {order['estimated_delivery']}\n" await message.answer(text, reply_markup=kb) await state.clear()
Интеграция с CRM через middleware
Чтобы crm был доступен в хендлерах как параметр:
# В __main__.py при инициализации
crm_service = CRMService(
api_url=settings.crm_api_url,
api_key=settings.crm_api_key,
)
# Передаём через workflow_data диспетчера
dp.workflow_data["crm"] = crm_service
Aiogram 3 автоматически инжектит зависимости из workflow_data — если хендлер принимает параметр crm, он получит экземпляр CRMService.
Уведомление менеджера о новой записи
async def notify_admin(bot, admin_id: int, booking_data: dict):
text = (
"<b>Новая запись</b>\n\n"
f"Услуга: {booking_data['service']['name']}\n"
f"Дата: {booking_data['date']}\n"
f"Время: {booking_data['time']}\n"
f"Телефон: {booking_data['phone']}\n"
f"Клиент: @{booking_data.get('username', '—')}"
)
await bot.send_message(admin_id, text)
Есть идея? Реализуем
Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

