Телеграм-бот, который принимает записи к специалистам на естественном языке. Пользователь пишет «Хочу к стоматологу в Казани на завтра утром», а LLM-агент сам находит врача, проверяет свободные слоты в расписании и создаёт запись — без кнопок, меню и пошаговых форм.
Ключевая идея проекта: бизнес-логика живёт в обычном REST API, а языковая модель получает к ней доступ через MCP (Model Context Protocol) — как к набору инструментов. Модель не знает про базу данных и не пишет SQL; она вызывает те же операции, которые доступны любому другому клиенту API.
Стек: Python 3.11 · aiogram 3 · LangChain + Ollama (qwen3:8b, локальная LLM) · MCP (mcp, mcp-use) · Django 4.2 + Django REST Framework · SQLite · Docker Compose
- Найти специалиста по специализации, городу или категории услуг
- Показать свободные слоты врача на конкретную дату (сетка по 30 минут, с учётом графика работы и уже занятых часов)
- Записать на приём — время окончания рассчитывается автоматически из длительности услуги
- Подтвердить, отменить или закрыть запись (
pending → confirmed → completed/cancelled) - Ответить на вопросы об услугах, ценах, расписании и категориях
- Держится в рамках роли: системный промпт ограничивает агента темой медцентра и записей
Каталог, расписания и записи параллельно доступны через админку Django и Swagger UI.
flowchart LR
subgraph bot["telegram_bot/"]
BOT["bot.py<br/>aiogram, polling"]
AGENT["MCPAgent<br/>до 30 шагов"]
MCP["mcp_server.py<br/>6 tools + 12 resources"]
BOT <--> AGENT
AGENT <-->|"MCP / stdio"| MCP
end
subgraph api["api/"]
DRF["Django REST API<br/>6 ViewSet'ов, бизнес-правила"]
DB[("SQLite")]
DRF <--> DB
end
TG(["Telegram"]) <--> BOT
AGENT <-->|"промпт / ответ"| OLLAMA["Ollama<br/>qwen3:8b"]
MCP <-->|"REST / JSON"| DRF
Полный цикл одного запроса:
sequenceDiagram
actor U as Пользователь
participant B as bot.py
participant A as MCPAgent
participant M as MCP-сервер
participant D as Django API
U->>B: «Запиши к стоматологу на завтра в 11:00»
B->>A: текст + Telegram ID отправителя
A->>M: search_specialists(specialization, city)
M->>D: GET /api/specialists/?search=...
D-->>M: список специалистов
M-->>A: результат
A->>M: get_available_slots(specialist_id, date)
M->>D: GET /api/specialists/{id}/available_slots/
D-->>M: свободные слоты
M-->>A: результат
A->>M: create_appointment(...)
M->>D: GET /api/services/{id}/ — длительность, расчёт end_time
M->>D: POST /api/appointments/
Note over D: validate(): пересечения<br/>и график работы
D-->>M: созданная запись
M-->>A: результат
A-->>B: текст ответа
B->>U: «Записал: завтра 11:00–11:30»
1. Бот (telegram_bot/bot.py) — тонкий слой. Принимает сообщение, добавляет к нему Telegram ID отправителя (чтобы агент мог связать собеседника с клиентом в базе), отдаёт MCPAgent. Ответ модели чистится от блока рассуждений <think>...</think>, которые генерирует qwen3, и уходит пользователю.
2. Агент — MCPAgent из mcp-use поверх ChatOllama. До 30 шагов цикла «рассуждение → вызов инструмента → анализ результата». Модель работает локально в Ollama: данные пациентов не уходят во внешние API.
3. MCP-сервер (telegram_bot/mcp_server.py) — мост между моделью и API, запускается через stdio.
- 6 tools (действия):
search_specialists,get_available_slots,create_appointment,confirm_appointment,cancel_appointment,complete_appointment - 12 resources (чтение): специалисты, их услуги и расписания, услуги, категории, записи, клиенты
- Docstring каждого инструмента — это и есть его спецификация для модели: по ним LLM понимает, что принимает аргумент
dateв форматеYYYY-MM-DD, аstart_time—HH:MM. - Самая содержательная логика здесь —
create_appointment: инструмент сам подтягивает длительность услуги и вычисляетend_time, чтобы модели не приходилось считать время.
4. Django REST API (api/) — источник правды и место, где живут инварианты.
- Модели:
ServiceCategory,Specialist,SpecialistSchedule,Service,Client,Appointment GET /api/specialists/{id}/available_slots/?date=...— генерация слотов: берётся график на нужный день недели, режется на интервалы по 30 минут, из них вычитаются пересечения с активными записямиAppointmentSerializer.validate()— двойная защита: запись не создастся, если время пересекается с существующей (pending/confirmed) или выходит за пределы графика работы специалиста. Даже если модель ошибётся, API её остановит.- Swagger/ReDoc через
drf-yasg
Почему MCP, а не function calling напрямую. Инструменты описаны один раз на стороне сервера и не привязаны ни к боту, ни к конкретной модели. Тот же mcp_server.py можно подключить к другому клиенту (Claude Desktop, IDE, веб-чат), а модель в боте заменить одной строкой — контракт не меняется.
| Модель | Назначение |
|---|---|
ServiceCategory |
Категории услуг (Красота, Здоровье, Фитнес) |
Specialist |
Врач/мастер: специализация, город, фото, привязка к User |
SpecialistSchedule |
График по дням недели (unique_together на специалиста и день) |
Service |
Услуга: цена, длительность в минутах, специалист, категория |
Client |
Клиент: телефон, email, город, telegram_id |
Appointment |
Запись: клиент, услуга, специалист, дата, интервал, статус |
- Docker и Docker Compose
- Ollama с загруженной моделью:
ollama pull qwen3:8b
- Бот в Telegram, созданный через @BotFather
Создайте .env в корне проекта:
BOT_TOKEN=<токен от BotFather>
OLLAMA_URL=http://host.docker.internal:11434host.docker.internal позволяет контейнеру достучаться до Ollama, запущенной на хосте (macOS/Windows). На Linux используйте http://172.17.0.1:11434 или запустите Ollama в той же Docker-сети.
docker compose up --builddocker compose exec api python manage.py migrate
docker compose exec api python manage.py create_test_dataКоманда create_test_data создаёт категории, специалистов с расписаниями, услуги и тестовых клиентов, а также администратора admin / admin.
- Swagger: http://localhost:8000/swagger/
- Админка: http://localhost:8000/admin/
- Бот: напишите ему
/startв Telegram
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# терминал 1 — API
cd api && python manage.py migrate && python manage.py create_test_data
python manage.py runserver 8000
# терминал 2 — бот (MCP-сервер поднимается им автоматически через stdio)
cd telegram_bot && python bot.py👤 Есть стоматологи в Казани?
🤖 Да, принимает Иванов Пётр Сергеевич — стоматолог, Казань.
Услуги: консультация (30 мин, 1500 ₽), лечение кариеса (60 мин, 4500 ₽).
👤 Какие есть окна на 15 июня?
🤖 Свободно: 09:00–09:30, 09:30–10:00, 11:00–11:30, 14:00–14:30.
👤 Запиши на консультацию в 11:00
🤖 Записал: 15 июня, 11:00–11:30, консультация у Иванова П. С.
Статус — ожидает подтверждения.
Что сделано осознанно:
- Локальная LLM. Медицинские данные не покидают инфраструктуру — для домена это не опция, а требование.
- Валидация на стороне API, а не промпта. LLM недетерминирована; пересечения слотов и выход за график ловятся в сериализаторе, где ошибиться нельзя.
- Расчёт
end_timeвнутри MCP-инструмента. Арифметика со временем — плохая задача для модели, поэтому она отдана коду. - Разделение tools и resources. Действия, меняющие состояние, отделены от чтения — модель не может «случайно» что-то создать, читая справочник.
Что осталось за рамками текущей версии:
- Аутентификация:
permission_classes = [AllowAny]и placeholder-токен в MCP-сервере — рабочая конфигурация для демо, но перед продакшеном нужен реальный Token/JWT-слой. mcp_server.pyходит наhttp://localhost:8000— при запуске в Docker Compose адрес нужно поменять наhttp://api:8000(или вынести в переменную окружения).- SQLite и
runserver— dev-конфигурация; для боевого контура нужны PostgreSQL и ASGI-сервер. telegram_idесть в моделиClient, но не попал вClientSerializer— связка «Telegram-пользователь ↔ клиент в базе» пока делается через передачу ID в промпте.- Нет истории диалога между сообщениями: каждый запрос обрабатывается независимо.
.
├── api/ # Django REST API
│ ├── boba/ # settings, urls, swagger
│ └── booking/
│ ├── models.py # 6 моделей предметной области
│ ├── serializers.py # валидация пересечений и графика
│ ├── views.py # ViewSet'ы, генерация слотов, экшены статусов
│ └── management/commands/create_test_data.py
├── telegram_bot/
│ ├── bot.py # aiogram + MCPAgent + Ollama
│ └── mcp_server.py # 6 tools + 12 resources поверх REST API
└── docker-compose.yaml