# call-center — лонгрид по архитектуре и флоу

> Документ собран по результатам сквозного ревью репозитория (25+ Python-сервисов, 6 вэб-UI, Asterisk-телефония, "AI-стек"). Задача документа — дать целостную картину: что здесь на самом деле есть, как это работает end-to-end, и где основной технический долг. Короткие архитектурные факты уже были описаны в `docs/architecture/*.md` — этот файл идёт глубже и местами поправляет/уточняет то, что там написано (см. раздел 8 "Расхождения с существующей документацией").

---

## 1. Что это за проект — TL;DR

**call-center** — омниканальная контакт-центр платформа собственной разработки: телефония (Asterisk), Telegram, WhatsApp, Webchat, Email, плюс встроенный "AI-оператор" первой линии и полноценный CRM/sales-модуль поверх того же ядра. Технически это не микросервисы в строгом смысле — это **модульный монолит, физически нарезанный на ~25 FastAPI-процессов, но использующий одну общую SQLAlchemy-схему и одну БД**. Сервисы регулярно напрямую читают/пишут таблицы, которыми формально владеют другие сервисы (пример: `customer_service` джойнит 6 чужих таблиц, `sales_service` пишет прямо в `Customer`, `supervisor_service` читает `Interaction`).

Есть ли здесь фронт, бэк и ML?

- **Фронт** — да, но без фреймворка: 6 отдельных vanilla-JS SPA (`ui/operator`, `ui/supervisor`, `ui/admin`, `ui/analyst`, `ui/sales`, `ui/login`), без сборщика, без TypeScript, ~28 000 строк JS суммарно. Самые крупные — `ui/analyst/app.js` (7 927 строк) и `ui/operator/app.js` (7 456 строк, включает встроенный SIP.js WebRTC-софтфон).
- **Бэк** — да, это основной объём кода: 25 FastAPI-сервисов на Python, SQLite по умолчанию / PostgreSQL для прод, событийная шина поверх RabbitMQ (transactional outbox/inbox), но **шина выключена по умолчанию** (`EVENT_BUS_ENABLED=False`).
- **ML** — здесь стоит быть точным: **это почти не ML, а оркестрация сторонних AI-API** (OpenAI-совместимый чат-эндпоинт, Yandex SpeechKit, ElevenLabs) плюс много ручных rule-based эвристик (regex/keyword-словари на русском и казахском). Единственный по-настоящему локальный ML-компонент — `faster-whisper` (модель Whisper) в отдельном сервисе `streaming_asr_sidecar_service`, и то это вспомогательный "partial transcript" ускоритель, а не основной ASR-путь. Поиск по базе знаний ("RAG") — это keyword-скоринг без эмбеддингов и без векторного индекса.

---

## 2. Состав системы (карта сервисов)

### 2.1 Инфраструктурные
| Сервис | Роль |
|---|---|
| `gateway` (`gateway/app.py`) | Единая точка входа: реверс-прокси `/proxy/{service}/{path}` → `SERVICE_URLS`, раздаёт статику всех 6 UI, шлёт алерты об ошибках в Telegram-бот |
| `auth_service` | Логин/пароль (в открытом виде!), OIDC/Keycloak PKCE-флоу, кастомный HMAC-токен (не JWT-библиотека, а самописный HS256-подобный формат) |
| `audit_service` | Лог аудита; пишется либо напрямую (`POST /audit/events`), либо через подписку на шину событий (которая по умолчанию выключена — см. §5) |
| `event_bus_service` | RabbitMQ dispatcher поверх transactional outbox/inbox |

### 2.2 Клиентские/роутинг
| Сервис | Роль |
|---|---|
| `customer_service` | Профиль клиента + кросс-канальные внешние идентичности (`CustomerExternalIdentity`) |
| `interaction_service` | Канонический "Interaction" (обращение/тикет) + таймлайн |
| `routing_service` | Распределение по очередям — **на деле не skill-based**, см. §7.3 |

### 2.3 Телефония и медиа-каналы
| Сервис | Роль |
|---|---|
| `asterisk_bridge_service` | Мост к Asterisk: AMI-события, FastAGI/IVR, управление звонком, хендофф в AI, запись |
| `ivr_service` | Граф IVR-флоу (DTMF state machine), не знает про Asterisk напрямую |
| `recording_service` | Хранилище записей звонков (SHA-256, локальный диск) |
| `voice_adapter_service` | Пассивный append-only лог voice-событий для идемпотентности bridge'а (не эквивалент interaction/state) |
| `telegram_adapter_service`, `whatsapp_adapter_service`, `webchat_adapter_service`, `email_adapter_service` | Входящие/исходящие интеграции по каналам |

### 2.4 AI и база знаний
| Сервис | Роль |
|---|---|
| `kb_service` | Хранилище статей + keyword-поиск (`services/shared/kb_search.py`) |
| `ai_orchestrator_service` | "Мозг": решает reply/handoff для Telegram, WhatsApp И голоса (все три канала — в одном сервисе) |
| `ai_voice_runtime_service` | Real-time аудио-слой: ASR/TTS, barge-in, AudioSocket-протокол |
| `streaming_asr_sidecar_service` | Локальный faster-whisper для partial-транскриптов (опциональный ускоритель) |
| `real_time_voice` | **Не отдельный сервис** — 15-строчный alias-реэкспорт `ai_voice_runtime_service.app:app` |

### 2.5 Аналитика/CRM/надзор
| Сервис | Роль |
|---|---|
| `reporting_service` | KPI-агрегация (SLA/ASA/AHT/Abandon/FCR) |
| `supervisor_service` | Live-мониторинг очередей и статусов агентов |
| `sales_service` | **Отдельный полноценный CRM** (лиды/сделки/оплаты/эскалации), 7 140 строк в одном `app.py`, 94 REST-эндпоинта — самый большой файл в репозитории, см. §7.4 |

---

## 3. Данные и схема

- По умолчанию — **SQLite** (`.data/mvp_cc.db`), для прод/K8s — **PostgreSQL**.
- Схема управляется **тремя параллельными механизмами одновременно**, которые надо вручную синхронизировать при любом изменении:
  1. SQLAlchemy ORM-модели (`services/shared/sql_models.py`, `sales_sql_models.py`) — единый `Base` на все "микросервисы".
  2. `migrations/sql/*_{postgres,sqlite}.sql` — 31 миграция × 2 диалекта = **62 файла**, написанные вручную (не codegen), почти механический перевод друг друга.
  3. `services/shared/sql_init.py::_apply_runtime_schema_compatibility()` — ~700-строчный императивный шим `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`, который выполняется при **каждом старте каждого сервиса**, чтобы "дотянуть" SQLite-схему до актуального состояния ORM.
- Миграции запускаются `scripts/migrate_core_db.py`; отслеживаются только по имени файла (без checksum), без down-миграций.
- Найдена коллизия номеров: `0013_asterisk_bridge_idempotency` и `0013_telegram_ai` — оба претендуют на номер 13.
- **Контракты (`contracts/openapi/*.yaml`, `contracts/events/*.json`) существуют только как документация — нигде не валидируются.** Нет `jsonschema`/`openapi-spec-validator` в зависимостях, event-конверты никем не сверяются со схемой перед публикацией.

---

## 4. Как это работает: два сквозных флоу

### 4.1 Входящий телефонный звонок (ring → AI или человек)

1. Оператор связи → Asterisk (`telecom-kz` trunk) → dialplan `[from-telecom]` жёстко маршрутизирует всё на extension `7100` (AI-first — это зашито в dialplan, а не конфигурируется).
2. `[mvpcc-call-init]`: генерируется `CALL_ID`/`LINKED_ID`, стартует запись (`MixMonitor`), летит AMI `UserEvent(MVPCCCallStarted)`.
3. `asterisk_bridge_service.ami.ami_loop()` (фоновый поток, персистентное AMI-соединение) ловит событие → пишет в `AsteriskEventLogRow` (inbox/outbox-паттерн с at-least-once доставкой) → `process_call_started`: создаёт `Interaction(channel="voice")` через `interaction_service`, создаёт/обновляет `AsteriskCallLinkRow` (телефонный state machine: `ringing → claimed → connected → ended`, плюс отдельный `ai_state`: `queued → greeting → active/handoff_required → human_owned/closed`).
4. Так как очередь сконфигурирована как `ai_first`, бридж стартует Voice AI сессию (`POST .../internal/voice-ai/sessions` в `ai_voice_runtime_service`), Asterisk запускает `AudioSocket()` — сырой TCP-канал аудио к рантайму.
5. Внутри `ai_voice_runtime_service`/`ai_orchestrator_service` идёт разговор (ASR → LLM-решение → TTS), подробности в §6.
6. Если AI решает передать на человека — вызывает бридж (`POST /internal/voice-ai/calls/{id}/handoff`), тот резолвит текущий live-канал (нетривиальная эвристика — Asterisk меняет `UniqueID` на каждом хопе `Dial`/`Redirect`), ставит `MVPCC_*` channel vars и делает AMI `Redirect` в `[mvpcc-transfer]`.
7. На пикапе оператором — `MVPCCOperatorConnected` → `process_operator_connected`: `AsteriskCallLinkRow` переходит в `human_owned`, в таймлайн обращения падает запись "AI запросил перевод на специалиста".
8. Хэнгап → `h`-extension: `StopMixMonitor` + `MVPCCCallEnded`/`MVPCCRecordingReady` → бридж скачивает файл (локально или по SFTP) и загружает в `recording_service`, который публикует `call.recording.ready` в реальную шину событий.
9. Если что-то из AMI-событий потерялось (обрыв AMI-сессии, рестарт бриджа) — фоновый `reconcile.py` каждые N секунд сверяет "зависшие" звонки через `CoreShowChannels` и синтезирует недостающие `call.ended`/`recording.ready`.

**Важное расхождение с документацией**: события `call.connected` и `call.transferred`, хотя и описаны в `contracts/events/`, **реально никогда не долетают до event-bus** — `emit_voice_event()` в бридже — это просто синхронный POST в `voice_adapter_service` (который только пишет лог-таблицу и не публикует в шину). `event_bus.ensure_rabbitmq_topology()` даже не биндит эти два типа событий ни к одной очереди. А `call.recording.ready`, наоборот, до шины доходит, но **со схемным дрейфом**: контракт требует поле `recording_path`, а реальный паблишер шлёт `file_name` — такого поля вообще нет.

### 4.2 Входящее сообщение в Telegram → создание обращения

1. `POST /integrations/telegram/bot/webhook` (защита — только статичный shared-secret заголовок, **без HMAC**, в отличие от WhatsApp).
2. Парсинг апдейта → дедуп по `external_message_id` (на случай повторной доставки вебхука).
3. `_upsert_thread_for_inbound`: резолвит/создаёт клиента через `CustomerExternalIdentity` (`telegram:{user_id}`), находит/создаёт `TelegramThreadRow`; если нет открытого треда — создаёт `Interaction(channel="telegram")` + таймлайн.
4. Два побочных эффекта: `_sync_sales_thread()` (синхронизация с CRM — **только у Telegram, у WhatsApp такой синхронизации нет**, необъяснённая асимметрия) и `_maybe_enqueue_ai_for_thread()` (если тред не забрал человек — просит `ai_orchestrator_service` сгенерировать ответ).
5. Исходящая доставка ответа идёт через отдельный фоновый worker с claim/lease/retry-backoff — этот же паттерн **дословно продублирован** в `whatsapp_adapter_service` под другими именами функций.

---

## 5. Событийная шина: что реально работает, а что нет

Документация (`docs/architecture/overview.md`, `event-schemas.md`) описывает event-driven архитектуру поверх RabbitMQ с transactional outbox/inbox — **это правда как код**, но не как поведение по умолчанию:

- `append_outbox_event()` действительно пишет событие в той же транзакции, что и доменные данные — это честный outbox.
- Но `EVENT_BUS_ENABLED` по умолчанию **`False`**. Диспетчер (`event_bus_service`), который открывает `pika`-соединение к RabbitMQ и реально публикует, стартует только если флаг включён.
- `audit_service` подписан на шину — но раз шина выключена по умолчанию, **аудит из доменных событий в демо/дефолтной конфигурации не работает вообще**.
- `interaction_service`/`supervisor_service` оборачивают запись в outbox условием `if event_bus_enabled()`, а вот `sales_service` — нет: пишет outbox-строки всегда, даже когда шина выключена (просто копятся неотправленные записи).

Итог: в дефолтной конфигурации это **набор сервисов, синхронно общающихся HTTP-вызовами и делящих одну БД**, а не событийная архитектура. Событийность — реальная, но опциональная фича, которая явно включается только в проде (и то не факт, что везде).

---

## 6. AI/ML-стек: честная оценка

**Главный вывод: это оркестрация сторонних AI-API + большой объём rule-based логики на regex/keyword-словарях, а не собственные ML-модели.** Единственное исключение — локальный `faster-whisper` в отдельном сервисе.

### 6.1 Кто с кем говорит

- **`ai_orchestrator_service`** — единственный "мозг" для **всех** текстовых/голосовых AI-веток: Telegram AI, WhatsApp AI и голосовой AI живут в одном `app.py` (3 717 строк) + `voice.py` (2 853 строки, специфика голоса). Из них добрая половина `app.py` (~1400 строк) — это аналитика/дэшборды (AI containment/handoff funnel), а не сама логика решений.
- LLM-вызов — не через официальный SDK, а сырой `httpx` POST на `{AI_API_BASE}/chat/completions` (OpenAI-совместимый формат, `AI_PROVIDER=openai_compatible`). По умолчанию `AI_PROVIDER=stub` — работает вообще без LLM, чисто на детерминированных правилах (`_stub_decision`): проверка идентичности → запрос человека → чувствительная тема → лучшее совпадение в KB → generic-уточнение.
- Хэндофф на человека — не отдельный классификатор, а поле `needs_handoff`, которое либо возвращает сама LLM в JSON-ответе, либо жёстко срабатывает раньше LLM по regex-правилам (`_looks_like_human_request`, `_is_sensitive_request`).
- **Голосовая ветка** (`voice.py`) — многослойная rule-based политика (проверка идентичности → "вы ещё здесь?" → счётчик уточнений ≥4 → off-domain-отказ → LLM-вызов только если явно включён `llm_guarded`/`v2_*` режим → фоллбэк на KB → generic-уточнение). Голос может полностью работать без единого обращения к LLM.

### 6.2 Реальный аудио-пайплайн (`ai_voice_runtime_service`)

- **ASR/TTS-провайдеры**, подтверждено по коду: **Yandex SpeechKit** (дефолт), **OpenAI**, **ElevenLabs** — всё через самописные `httpx`/`grpc`-клиенты (нет `openai`/`anthropic` пакетов в `requirements.txt` вообще — ни одной официальной SDK).
- **AudioSocket** — протокол Asterisk для стриминга аудио, реализован вручную (3-байтовый заголовок + payload).
- **VAD (детекция речи) и barge-in** — не ML, а простой **RMS-энергетический порог** (`EnergyVAD`): если энергия кадра выше порога дольше 220 мс во время проигрывания ответа бота — прерываем воспроизведение. Ни Silero-VAD, ни WebRTC-VAD — ничего ML-based здесь нет.
- `ack_bank.py` — файловый кэш коротких "заполняющих" фраз ("секунду, посмотрю...") с ключом по SHA-256, чтобы маскировать задержку LLM/TTS без живого TTS-раунд-трипа на каждую реплику.

### 6.3 "RAG" по базе знаний

`services/shared/kb_search.py` — **не векторный поиск**. Загружает все строки в Python и скорит каждую вручную (совпадение токенов + префиксный fuzzy-match + бонус за фразу), без обратного индекса, без учёта частоты термина (IDF), без SQLite FTS5/pgvector. При росте базы знаний это не масштабируется — сейчас это полный скан таблицы на каждый запрос.

### 6.4 Единственный настоящий локальный ML-компонент

`streaming_asr_sidecar_service` — отдельный FastAPI-сервис, реально грузит модель **`faster-whisper`** (`WhisperModel`, CPU, int8-квантование по умолчанию). Используется только как опциональный "partial transcript" ускоритель в режиме `voice v2` (быстрые промежуточные транскрипты для более ранней реакции бота), финальный/авторитетный транскрипт по умолчанию всё равно идёт через облачный Yandex SpeechKit.

### 6.5 Найденные проблемы (детали ниже в §7)

- Мёртвый дублированный код в `voice.py` (закрытая копия `_voice_decision`, полностью неиспользуемая `_voice_decision_legacy`).
- Три почти идентичных модуля "singleton JSON config в БД" (`ai_operator_config.py`, `voice_tts_config.py`, `voice_name_config.py`).
- God-class на 1954 строки (`media_runtime.py`) — протокол, VAD, barge-in, стриминг ASR, воспроизведение и метрики в одном классе.
- Казахстанские гео-специфичные словари (город, алиасы) захардкожены прямо в `services/shared/ai_context_summary.py` — не вынесены в конфиг, что мешает мультирегиональному переиспользованию.
- Три независимых механизма кэширования TTS, решающих одну и ту же задачу по-разному (offline-материализация IVR-промптов, live-синтез, `ack_bank`).

---

## 7. Технический долг и находки (сведено по приоритету)

### 7.1 🔴 Безопасность: подмена ролей через заголовки

`get_actor()` (`services/shared/security.py`) принимает **либо** подписанный Bearer-токен, **либо**, если `ALLOW_LEGACY_HEADER_AUTH=True` (это **дефолт**), сырые несигнированные заголовки `X-User`/`X-Role`. Gateway (`gateway/app.py::_forward`) **слепо форвардит** любые `X-User`/`X-Role`, которые прислал клиент, без сверки с токеном. Итог: **любой клиент, достучавшийся до `/proxy/*`, может выставить себе `X-Role: admin` и пройти любую `require_roles()`-проверку**, пока legacy header auth явно не выключен во всех окружениях. В UI это ровно та лазейка, о которой предупреждает `access-guards.js` (клиентский guard в `ui/operator` — чисто косметический, обходится правкой `localStorage`). Судя по `deployment/helm/values.track9-strict.yaml`/`values.track9-live.yaml` (`allowLegacyHeaderAuth: "0"`, `authMode: bearer`), это уже осознанная проблема, которую в проде отключают через overlay — но дефолт остаётся небезопасным.

### 7.2 🔴 Пароли и секреты

- `auth_service`: пароли пользователей сравниваются и хранятся **в открытом виде** (`user.password != payload.password`), нет ни одного вызова хэш-функции нигде в auth-пути. Демо-пользователи вида `admin123` захардкожены в сидинге.
- `services/shared/security.py`: секрет токена по умолчанию `"dev-secret-change-me"`, если `APP_TOKEN_SECRET` не задан — тихий даунгрейд безопасности при забытой переменной окружения.
- `deployment/docker-compose.asterisk.server.yml`: реальный продовый IP (`92.38.48.166`) захардкожен прямо в compose-файле.

### 7.3 🟠 Routing на деле не skill-based

`routing_service` парсит поле `strategy` (`round_robin|least_loaded|skill_based`) из правил очереди, но **реально использует только round-robin** по захардкоженному в коде словарю `_AGENTS = {"voice": [...], "telegram": [...], ...}` (6 фейковых username). `least_loaded`/`skill_based` — мёртвые значения enum, не влияющие ни на что. Состояние агентов (online/busy) вообще хранится в другом сервисе (`supervisor_service`) и никогда не читается роутингом — это два несвязанных подсистемы, случайно делящие общее событие `agent.state.changed`.

### 7.4 🟠 `sales_service` — параллельный продукт, слабо связанный с ядром

7 140 строк в одном `app.py`, 94 эндпоинта — крупнейший файл в репозитории на порядок. Полноценный CRM (лиды/сделки/33 стадии в явном state machine/платежи с HMAC-верификацией вебхуков/эскалации) с собственным мульти-тенантным слоем (`tenant_id` на каждой таблице — единственное место в системе, где мультитенантность реально используется). Стыкуется с ядром только в одной точке — таблице `Customer`, куда пишет напрямую, **в обход `customer_service`** (та же таблица, но два независимых write-пути без единой каскадной логики — при переименовании клиента через `customer_service` каскадно обновляются 3 связанные таблицы, через `sales_service` — нет). Отдельно стоит отметить: `automation_worker.py` (695 строк, полноценная task-queue с retry/backoff) **нигде не инстанциируется вне тестов** — задачи автоматизации (напоминания по счетам, эскалации) складываются в таблицу и никогда не обрабатываются в проде.

### 7.5 🟠 Дублирование кода в адаптерах каналов

- `webchat_adapter_service` и `email_adapter_service` — структурно идентичные файлы (120 и 110 строк), различаются только именами полей.
- `telegram_adapter_service` (2 145 строк) и `whatsapp_adapter_service` (2 328 строк) — почти построчное зеркало друг друга (одинаковые имена функций, одинаковый порядок): резолв клиента, рендер AI-саммари, worker доставки ответов с claim/lease/retry — всё продублировано. Оценка потенциального сокращения при вынесении общего `chat_adapter.py` в `services/shared/` — **~1500–1800 строк**.
- 6 UI-приложений (`ui/*/app.js`) независимо реализуют одинаковые `persistSession()`/`api()`/`log()`/`escapeHtml()` — без единого модуля, потому что нет сборщика и ES-модулей. При этом обработка истёкшего токена (авто-логаут на 401) есть **только** в `ui/operator` — остальные 5 приложений просто тихо продолжают фейлить запросы после истечения токена.

### 7.6 🟡 Мёртвый/дублированный код (конкретные находки)

| Файл | Проблема |
|---|---|
| `services/ai_orchestrator_service/voice.py:1785-1900` | Закрытая (shadowed) первая версия `_voice_decision`, вторая версия на строке 1903 её перекрывает — редактировать первую бессмысленно |
| `services/ai_orchestrator_service/voice.py:1697-1783` | `_voice_decision_legacy` — нигде не вызывается |
| `ui/analyst/app.js` | Минимум 6 функций продублированы дважды в файле (`loadSavedAnalyticsViews`, `saveAnalyticsView`, `deleteAnalyticsView`, `loadAnalyticsTrend` и др.) — похоже на не до конца слитые две версии модуля аналитики; вторая версия по факту рабочая, первая — мёртвый вес (сотни строк) |
| `ui/operator/sip-0.21.2.min.js` vs `ui/operator/vendor/sip-0.21.2.min.js` | Побайтово идентичный дубль, `vendor/`-копия сиротская, ничем не используется |
| `deployment/kubernetes/mvp-cc-platform.yaml` | Устаревший ручной K8s-манифест, не содержит 5 из текущих сервисов (`sales`, `ai-orchestrator`, `ai-voice-runtime`, `whatsapp-adapter`, `streaming-asr-sidecar`) — судя по всему, вытеснен Helm-чартом, но не удалён |
| `services/shared/ai_operator_config.py`, `voice_tts_config.py`, `ai_orchestrator_service/voice_name_config.py` | Три почти идентичных модуля "singleton JSON row в БД" — кандидат на общий `SingletonJSONConfigStore[T]` |

### 7.7 🟡 Инфраструктура/CI: следы миграций, которые не довели до конца

- **3 параллельных CI-системы** (GitHub Actions, GitLab CI, Gitea) — реально используется только GitLab CI (полный test→build→deploy, включая 5 deploy-джобов и live-патч прод-контейнера в обход образа — `hotfix-analyst-assets`). GitHub Actions не имеет деплоя вообще, Gitea вызывает непрозрачный внешний скрипт. Похоже на артефакт смены CI-провайдера.
- **"Track 9"** — конкретное историческое событие (переход на реальный Asterisk PBX на "scale500" в K8s + переключение auth с legacy-заголовков на bearer-токены). Скрипты `track9_*.py/.ps1` и Helm-оверлеи `values.track9-*.yaml` — это тулинг под один конкретный cutover, не переиспользуемые операционные скрипты; кандидаты на архивацию, если cutover завершён.
- Параллельно живёт "теневой" Postgres-стек (`docker-compose.parallel.server.yml`, отдельный env-темплейт) — явно инфраструктура идущей миграции SQLite → PostgreSQL, а не постоянная часть архитектуры.
- 5 файлов `.env*.template` с реальным дрейфом контента между `.env.example` и `.env.production.template` (не просто "прод — подмножество", а разные наборы переменных в обе стороны).

### 7.8 🟢 `asterisk_bridge_service` — архитектурный узел

Самый сложный сервис (5 790 строк суммарно по всем файлам). Каждый вынесенный в отдельный файл модуль (`ami.py`, `bridge_processing.py`, `voice_ai.py`, `call_control.py`, `ivr_fastagi.py`, `reconcile.py`, `recording_io.py`) на деле не независим — каждый определяет `_bridge_app()`, который лениво импортирует `app.py` и лезет обратно за конфигом/функциями/даже реэкспортами stdlib (`bridge.datetime`). Это не модульность, а один файл на 5 790 строк, искусственно нарезанный, с `app.py` в роли service-locator/mediator и реальным циклическим импортом, который обходят через ленивый импорт внутри функций. Резолв "какой live-канал сейчас соответствует этому звонку" реализован независимо 3 раза (`call_control.py` дважды, `voice_ai.py` один раз) вместо общей функции.

---

## 8. Расхождения с существующей документацией (`docs/architecture/*.md`)

- **overview.md** говорит "шина RabbitMQ — канонический механизм обмена" — верно как код, но не как поведение по умолчанию (шина выключена, см. §5).
- **voice-ai.md** описывает флоу верно на уровне намерения, но не упоминает, что голосовая ветка может полностью работать без обращения к LLM (rule-based fallback), и что часть "AI Summary" для оператора рендерится прямо в `asterisk_bridge_service/voice_ai.py` с захардкоженными русскими keyword-словарями, а не в `ai_orchestrator_service`, как можно было бы предположить по разделению ответственности из документа.
- **real-time-voice-service.md** — подтверждено ревью: `services/real_time_voice/app.py` действительно 15-строчный alias, без расхождений. Единственное уточнение: как самостоятельная директория сервиса это создаёт путаницу при первом знакомстве с деревом каталогов (выглядит как полноценный сервис).
- **event-schemas.md** перечисляет `call.connected.json`/`call.transferred.json` как часть контракта шины — по факту эти два события до шины не доходят вообще (см. §4.1). `call.recording.ready.json` доходит, но с несовпадающей схемой (`recording_path` vs `file_name`).

---

## 9. Резюме: что стоит делать в первую очередь

Если приоритизировать по риску/выгоде (без учёта уже сделанных в этой сессии небольших правок, см. отдельное сообщение с их списком):

1. **Безопасность** — выключить legacy header auth по умолчанию (или хотя бы явно предупредить/логировать при старте, что он включён), захэшировать пароли в `auth_service`.
2. **Контракты событий** — либо чинить `call.connected`/`call.transferred`/`call.recording.ready`, либо выпилить их из `contracts/events/`, чтобы документация не врала.
3. **Дублирование адаптеров каналов** — вынести общий `chat_adapter.py` для telegram/whatsapp (наибольший ROI по сокращению кода — полторы-две тысячи строк).
4. **`ui/analyst/app.js`** — вычистить задублированные функции (быстрая, низкорисковая правка, ощутимо уменьшает файл).
5. **`asterisk_bridge_service`** — если сервис ещё будет активно дорабатываться, стоит разорвать `_bridge_app()`-индирекцию через явный контекст/DI, иначе долг будет только расти вместе с файлом.
6. Архивировать/переименовать "track9"-скрипты и устаревший `deployment/kubernetes/mvp-cc-platform.yaml`, если Helm — единственный актуальный путь деплоя в K8s.






