Бот-ассистент для бизнеса: FAQ, запись, статус заказа

Бот-ассистент закрывает три задачи: отвечает на частые вопросы, записывает клиентов на услуги и показывает статус заказа. Без менеджера, 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)

Есть идея? Реализуем

Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

Написать в Telegram

29.03.2026

Нужна консультация?

Оставьте свои контактные данные, или свяжитесь с нами удобным для вас способом

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Нажимая кнопку «Принять», вы соглашаетесь на сбор cookie. Мы используем их для обеспечения функционирования веб-сайта, аналитики действий и улучшения качества обслуживания. Если Вы не хотите, чтобы эти данные обрабатывались, отключите cookie в настройках браузера или прекратите использовать сайт.
Принять