# AI CRM · Недвижимость

`01 · ai-crm · Delivered`

Production AI-агент с MCP-style 19-endpoint tool API, audit trail и operator handoff. Квалифицирует лидов на недвижимости, делает research, бронирует просмотры.

**Скоуп:** Соло · 7 недель  
**Роль:** Fullstack-платформа с автономным Telegram-агентом

**Видео:** [YouTube](https://www.youtube.com/watch?v=0pK0MmDL0rc) · [RuTube](https://rutube.ru/video/private/23fbb6db856a945cc2357befda717669/?p=JVCWjl-ogS4oKbeKhEywnw)

## Разбор видео

Production AI-агент ведёт чат с клиентом в Telegram, операторы супервизят через админку с capability-моделью прав — Канбан-пайплайн, календарь, квалификация лидов, глубокий ресёрч проектов, бронирование просмотров, передача оператору. Каждый AI-вызов трекается: токены и стоимость видны по клиенту, отдельно для AI, общающегося с клиентом, и помощника оператора.

CRM с AI-риелтором для агентств недвижимости — клиенты переписываются с ним в Telegram, а команда видит всю работу в одном окне.

Дашборд показывает сделки, лидов и нагрузку команды. Канбан-доска ведёт пайплайн — перетащите карточку между этапами, и история сохранится сама.

Каждая карточка живёт и в календаре. Фильтруйте по оператору или доске, переключайтесь между месяцем, неделей или днём, открывайте любое событие — детали и связанные карточки на месте.

Из карточки переходим сразу в чат. AI квалифицирует лида, находит подходящие проекты, проводит глубокий ресёрч и отправляет интерактивную карточку объекта прямо в Telegram. Клиент смотрит фото, юниты и отчёты не выходя из приложения. Когда клиент готов, AI бронирует просмотр и сохраняет заметку с контекстом для команды.

Когда за дело берётся человек — AI останавливается. У операторов есть свой AI-помощник внутри системы — спросите про клиента, и он вернётся с кратким резюме и готовым ответом, опираясь на всю историю переписки.

Каждый запрос отслеживается: токены и стоимость видны по каждому клиенту, отдельно по AI, который общается с клиентом, и отдельно по помощнику оператора.

Система также поддерживает светлую и тёмную темы и набор акцентных цветов.

---

## Контекст

> Звонки и просмотры ведут операторы. Всё остальное делает AI.

Агентства недвижимости, работающие на Telegram-inbound, принимают сообщения круглосуточно. Объём — больше того, что небольшая команда операторов способна вытянуть. Каталоги в тысячи проектов и десятки тысяч юнитов лежат за пределом того, что человек держит в голове в диалоге. Готовые чат-боты отвечают одним FAQ и встают; никто не доводит покупателя от первого сообщения до забронированной встречи.

Разделение фиксированное: операторы ведут звонки и просмотры; всё остальное — квалификация, глубокий research по проекту и району, бронирование под расписание операторов — работает без них.

## Факты

| | |
|---|---|
| **Скоуп** | 7 недель соло |
| **Поверхности** | CRM-админка + Telegram WebApp (1 репо · 2 Vite entry) |
| **Каталог** | Тысячи проектов · десятки тысяч юнитов · синк из GenieMap · raw_payload сохранён |
| **Agent tools** | 19 собственных эндпоинтов + 5 callback’ов · Bearer-auth · единый envelope |
| **Модель прав** | 15 capability-ключей |
| **Статус** | Delivered · 20 pytest-модулей · structlog · respx |

## Архитектура

### Жизненный цикл сообщения

```text
 1  Клиент                          Сообщение в Telegram
        │
 2  tg_bot
        │  POST /api/v1/conversations/messages/        [BOT_SHARED_TOKEN]
        ▼
 3  Backend  (Django/DRF)
        │  upsert Conversation + Message[RECEIVED]
        │  Celery.dispatch_to_n8n_workflow.delay()
        ▼
 4  Celery worker
        │  POST {N8N_BASE_URL}/{webhook}               [N8N_API_KEY]
        ▼
 5  n8n  ──►  LLM
                │  tool_call  (projects-search, send-webapp, …)
                ▼
              Agent Tools API                          [Bearer]
                │  data  {data, meta, errors}
                ▼
              LLM  ──►  финальный текст ответа
        │
 6  Backend  ◄── callback /api/v1/integrations/n8n/    [N8N_CALLBACK_TOKEN]
        │  Message[AWAITING_CALLBACK → READY_TO_SEND]
        ▼
 7  Celery worker
        │  POST /bridge/send                           [TG_BRIDGE_TOKEN]
        ▼
 8  Telegram → Клиент            Message[SENT]
```

**Состояния сообщения (7).** Линейный путь: RECEIVED → PROCESSING → AWAITING_CALLBACK → READY_TO_SEND → SENT. Ветки: AWAITING_OPERATOR (AI-toggle off в середине диалога), FAILED (любая n8n-ошибка).

**Зачем отдельный bridge.** Telegram-сессию бота держит один процесс — tg_bot. Backend не открывает свою сессию; исходящее уходит через HTTP-bridge внутри того же контейнера. Backend остаётся stateless относительно Telegram.

**Где живёт AI.** Внутри шага 5 — LLM tool-loop живёт в n8n. Backend обслуживает и callback’и, и сами tool-вызовы LLM в процессе loop’а. (Зачем тут n8n — см. §04-Decisions.)

### Компонентная схема

```text
   External            tg_bot                   frontend  (admin · webapp)
   ────────            ──────                   ───────────────────────────
   Telegram   ◄─────►  aiogram + bridge         React · Vite (×2 entries)
                            │                          │
               BOT_SHARED ──┤                          │  HTTPS · /api/v1
               TG_BRIDGE  ──┤                          │
                            ▼                          ▼
                     ┌───────────────────────────────────────────┐
                     │   Backend  (Django · DRF · structlog)     │
                     │   Единый envelope:  {data, meta, errors}  │
                     └────┬─────────┬──────────┬─────────────────┘
                          │         │          │
                          ▼         ▼          ▼
                    Postgres 16  Redis 7   Celery worker + beat
                                               │  N8N_API_KEY
                                               ▼
                                      n8n + n8n-worker
                                      (LLM tool-loops)
                                               │  N8N_CALLBACK_TOKEN
                                               ▼
                                         Backend.callback
```

**Девять Docker-сервисов.** postgres · redis · backend · celery · celery-beat · tg_bot · frontend · n8n · n8n-worker. (n8n-worker — отдельный процесс-исполнитель; разгружает оркестратор от долгих flow.)

**Модель доверия.** Каждый межсервисный вызов несёт явный shared secret — bot-bridge, n8n outbound, n8n callback, agent-tool Bearer. Ни один сервис не доверяет другому только по тому, что они в одном compose-файле.

**Разделение фронта.** Один Vite-репо, два entry HTML — /admin (CRM) и /webapp/:projectId (Telegram WebApp). Компоненты и API-клиент общие. WebApp работает внутри Telegram через WebApp SDK; контроль доступа — на тех же backend endpoint-правилах, что и admin.

### Доменное ядро

```text
CATALOG                          CONVERSATIONS
───────                          ─────────────
Project                          Conversation
  ├── Unit (×N)                    · status
  ├── ResearchBlock × 4            └── Message (×N, 7-state lifecycle)
  │     · core                   Note (manual + 3 system cards)
  │     · market & demand        OperatorAssistantSession
  │     · legal & ops
  │     · dynamics & news
  ├── Amenity (×N)
  └── District (FK)              KANBAN
                                 ──────
USERS                            Board
─────                              └── Column
User · capability strings               └── Card
     · telegram_link                         └── Activity
UserInvite
                                 CALENDAR
GENIEMAP                         ────────
────────                         Calendar
ProviderSyncLog                    ├── CalendarEvent
  · raw_payload preserved          └── ScheduleRule
  · scheduled syncs
```

**37 моделей в 7 apps.** Домен разнесён по bounded context’ам: catalog · conversations · kanban · calendar · users · geniemap · auth. 5 apps без моделей (core, agent_tools, dashboard, webapp, echo). Никакой god-table — каждая сущность отвечает за одну работу.

**Status-enum’ы везде, не bool-флаги.** У каждого домена свой типизированный status — Project (launch · available · sold_out · cancelled), Unit (available · reserved · sold · on_hold), Message (7-стейтная machine), Kanban Card (open · closed_won · closed_lost). Одна проецируемая поверхность на домен вместо парада `archived: bool`, `is_active`, `is_draft`.

**raw_payload сохранён.** Каждая сущность, синканная из GenieMap, хранит исходный JSON рядом с нормализованными колонками. Переживает любой mapping drift, делает re-ingest дешёвым.

### Топологии агентов

![Четыре research-агента-специалиста (core, market & demand, legal & ops, dynamics & news), у каждого свой LLM, общий tool web-search.](https://ilyadev.xyz/private/airea-n8n-specialists.png)

*Кластер специалистов, общий tool*

![Один chat-агент с двенадцатью tools — projects/districts search и research, calendar availability и create, send-webapp, admin-notify, context-save, user-note CRUD, web-search.](https://ilyadev.xyz/private/airea-n8n-omnimodel.png)

*Omnimodel, двенадцать tools*

**За счёт чего это работает.** 19 эндпоинтов модель-агностичны и оркестратор-агностичны. Протестировано с GPT и Gemini против того же tool API.

## Ключевые инженерные решения

### 01 · Capability-overrides поверх role-пресетов

**Решение.** Три role-пресета (OWNER · ADMIN · OPERATOR) задают дефолтный набор прав; per-user extra_capabilities и revoked_capabilities дают точечные override. Проверки идут через hasCapability(), не role == "admin".

**Почему.** Реальные комбо прав не ложатся в 3-ролевой enum — «дашборд — да, управление пользователями — нет, AI-toggle — да» норма. Чистый role-table взорвался бы; чистые capabilities теряют удобство дефолта. Override-модель сохраняет и то, и другое.

**Цена.** Два слоя при дебаге доступов — пресет + override’ы. Дефолтные пресеты пришлось зафиксировать в коде ради UX out-of-the-box.

### 02 · 19 собственных agent tools на Bearer + единый envelope

**Решение.** Все AI tool-вызовы идут через /api/v1/agent/tools/... со своим Bearer-токеном; каждый ответ — единый envelope {data, meta, errors}.

**Почему.** n8n работает без пользовательского контекста — сессионная авторизация не подходит. Bearer-edge даёт LLM минимум прав, а единый envelope делает tool-loop стабильным.

**Цена.** Несколько эндпоинтов дублируется по аудитории (оператор vs AI) — две поверхности вместо одной с двойной авторизацией.

### 03 · n8n ради no-code surface

**Решение.** Логика агента живёт в 4 n8n-workflow; LLM tool-вызовы расходятся в собственный tool-API, callback возвращается через webhook.

**Почему.** Constraint: no-code surface для правок agent-flow. n8n на тот момент был очевидным кандидатом: популярный, зрелый, с нативным tool-calling и webhook-примитивами. Альтернатива — кастомный admin-UI поверх in-process tool-loop — удвоила бы scope MVP.

**Цена.** Логика промптов и оркестрации размазана между n8n-flow и собственным tool-API; n8n-worker — отдельный сервис. Ретроспективная оценка — в §06-Lessons.

### 04 · Research как state-machine из 4 блоков

**Решение.** Research проекта разбит на 4 блока (core, market & demand, legal & ops, dynamics & news), каждый цикличен EMPTY → PENDING → READY | FAILED. Callback’и от n8n приносят каждый блок в backend по мере готовности, и research-панель проекта рендерит их блок за блоком по мере поступления.

**Почему.** Глубокий research — несколько минут; блокировать tool-loop LLM нельзя, чат в Telegram замёрзнет. Поблочный UX показывает частичные результаты.

**Цена.** Каждый блок — со своим промптом и источниками поиска; поддержка research-пайплайна растёт линейно от числа блоков.

## Стек

| | |
|---|---|
| **Backend** | Python · Django · DRF · Celery · structlog · aiogram |
| **Frontend** | React · Vite · Tailwind 4 · Radix UI · MapLibre GL · react-router 7 |
| **Инфра** | PostgreSQL · Redis · Docker Compose |
| **AI · оркестр.** | Gemini, OpenAI · n8n (model-agnostic) |
| **Agent surface** | 19 собственных tool-эндпоинтов + 5 callback’ов · Bearer-auth · envelope {data, meta, errors} |
| **Объём** | 12 Django-apps · 224 фронт-модуля · 20 pytest-модулей · ~56K LOC |

## Уроки и статус

### Оставлю как есть

- Capability-overrides поверх role-пресетов — базовая роль даёт здравый дефолт, per-user extra/revoked-ключи закрывают длинный хвост без новых ролей.
- Единый envelope `{data, meta, errors}` на каждый API-ответ — скучный tool-loop это фича, а не баг.
- UX операторской консоли — канбан + чат + календарь в одном шелле, с on-request передачей живому оператору внутри того же треда.
- Дисциплина audit-trail — X-Request-ID, проброшенный через structlog на каждом межсервисном шаге. Без него debug LLM tool-loop разваливается.

### Поменял бы

- Django был выбран по рефлексу. Реально использованная поверхность — ORM, роутинг, организация по apps — осталась тонкой, а DRF добавлял boilerplate на каждый endpoint, и class-based permissions конфликтовали с capability-строками. FastAPI + Pydantic + async SQLAlchemy лучше легли бы на API-only форму, которой проект в итоге оказался.
- n8n себя не отбил — большинство правок флоу всё равно жили в промптах или в tool API. Сегодня сделал бы in-process tool-loop и тонкий кастомный UI ровно под те части, которые реально редактируются.
- 9 Docker-сервисов — много для MVP. Сегодня: n8n + n8n-worker уходят (in-process tool-loop), celery-beat сворачивается в celery как periodic, frontend в проде — статика за backend, а не dev-контейнер. Получается 4 сервиса: postgres, redis, backend, celery.

Собран, развёрнут, передан клиенту по итогу контракта; code walkthrough по запросу.

---

Источник: https://ilyadev.xyz/cases/ai-crm (HTML) · /cases/ai-crm.ru.md (этот файл)
Дальше: 02 — AI-агент для ресторанного склада → https://ilyadev.xyz/cases/ai-warehouse.ru.md
Индекс: https://ilyadev.xyz/llms-ru.txt — полный список кейсов
Автор: Илья Казанцев — https://ilyadev.xyz/index.ru.md
