docs: recreate documentation structure with updated specs
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
APP_IMAGE=registry.example.com/group/project/app:latest
|
||||
ASTERISK_IMAGE=registry.example.com/group/project/asterisk:latest
|
||||
@@ -1,91 +0,0 @@
|
||||
# API Matrix by Stage
|
||||
|
||||
## Stage 1
|
||||
- auth-service: `/auth/login`, `/auth/me`, `/users`
|
||||
- audit-service: `/audit/events`
|
||||
- all services: `/health`
|
||||
|
||||
## Stage 2
|
||||
- customer-service: `/customers`
|
||||
- interaction-service: `/interactions`
|
||||
- routing-service: `/queues`
|
||||
- voice-adapter-service: `/integrations/voice/events`
|
||||
- telegram-adapter-service: `/integrations/telegram/webhook`
|
||||
|
||||
## Stage 3
|
||||
- kb-service: `/knowledge/*`
|
||||
- reporting-service: `/reports/kpi`, `/reports/export`
|
||||
- supervisor-service: `/supervisor/realtime`
|
||||
|
||||
## Stage 4
|
||||
- recording-service: `/recordings`, `/recordings/*`
|
||||
|
||||
## Stage 5
|
||||
- ivr-service: `/ivr/flows`, `/ivr/flows/*`, `/ivr/sessions`, `/ivr/sessions/*`
|
||||
- routing-service: `/queues/{queue_id}/route` with optional `ivr_session_id`
|
||||
|
||||
## Stage 6
|
||||
- reporting-service: `/reports/kpi` with optional `channel`
|
||||
- reporting-service: `/reports/coverage`
|
||||
- reporting-service: `/reports/export` with optional `queue_id` and `channel`
|
||||
|
||||
## Stage 7
|
||||
- no new business REST routes
|
||||
- ops CLI:
|
||||
- `scripts/load_test.py` with profile-driven mixed workload
|
||||
- `scripts/track7_check.py` for scale/hardening evidence validation
|
||||
|
||||
## Stage 8
|
||||
- no new business REST routes
|
||||
- event-bus-service (ops only): `/bus/outbox`, `/bus/outbox/*`
|
||||
- ops CLI:
|
||||
- `scripts/event_bus_smoke.py`
|
||||
- `scripts/track8_check.py`
|
||||
|
||||
## Stage 9
|
||||
- no new business REST routes
|
||||
- recording-service: `/recordings/import-upload` (internal bridge upload)
|
||||
- asterisk-bridge-service (ops only): `/asterisk/status`, `/asterisk/events`, `/asterisk/events/*`
|
||||
- bridge-to-platform auth modes:
|
||||
- `legacy_headers` (QA baseline)
|
||||
- `bearer_first` / `bearer` (Track 9.1 hardening)
|
||||
- ops CLI:
|
||||
- `scripts/track9_preflight.py`
|
||||
- `scripts/asterisk_lab_smoke.py`
|
||||
- `scripts/track9_check.py`
|
||||
- `scripts/track9_collect_evidence.py`
|
||||
- `scripts/track9_2_cutover.ps1` (controlled Helm cutover automation)
|
||||
|
||||
## Stage 11
|
||||
- no new business REST routes
|
||||
- asterisk-bridge-service call-control routes:
|
||||
- `/asterisk/live-calls`
|
||||
- `/asterisk/live-calls/{call_id}/claim`
|
||||
- `/asterisk/live-calls/{call_id}/hangup`
|
||||
- `/asterisk/live-calls/{call_id}/blind-transfer`
|
||||
- `/asterisk/live-calls/{call_id}/actions`
|
||||
- operator shell uses these routes for live call handling with external softphone media
|
||||
|
||||
## Stage 12
|
||||
- no new business REST routes
|
||||
- additive operator UX route:
|
||||
- `/asterisk/recent-calls`
|
||||
- operator shell uses this route for the short-lived recent-calls bucket
|
||||
|
||||
## Stage 14
|
||||
- no new business REST routes
|
||||
- additive browser softphone config route:
|
||||
- `/asterisk/browser-softphone/config`
|
||||
- operator shell may use direct browser media over Asterisk WSS in QA
|
||||
|
||||
## Stage 15
|
||||
- telegram-adapter-service real-bot webhook:
|
||||
- `/integrations/telegram/bot/webhook`
|
||||
- telegram-adapter-service operator thread workspace:
|
||||
- `/integrations/telegram/threads`
|
||||
- `/integrations/telegram/threads/{thread_id}`
|
||||
- `/integrations/telegram/threads/{thread_id}/messages`
|
||||
- `/integrations/telegram/threads/{thread_id}/claim`
|
||||
- `/integrations/telegram/threads/{thread_id}/close`
|
||||
- `/integrations/telegram/threads/{thread_id}/escalate`
|
||||
- operator shell gets a separate `Telegram` page as the canonical surface for this channel
|
||||
@@ -1,59 +0,0 @@
|
||||
# Asterisk UserEvent Contract
|
||||
|
||||
Track 9 uses dialplan-generated `UserEvent` frames over AMI as the stable bridge contract
|
||||
between Asterisk and the platform.
|
||||
|
||||
The bridge listens only for `UserEvent` names starting with `MVPCC`.
|
||||
|
||||
## `MVPCCCallStarted`
|
||||
|
||||
Required fields:
|
||||
|
||||
- `CallID`
|
||||
- `LinkedID`
|
||||
- `CallerNumber`
|
||||
- `CallerName`
|
||||
- `QueueCode`
|
||||
- `Extension`
|
||||
- `Context`
|
||||
- `Direction`
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Event: UserEvent
|
||||
UserEvent: MVPCCCallStarted
|
||||
CallID: 1740912000.12
|
||||
LinkedID: 1740912000.12
|
||||
CallerNumber: 1001
|
||||
CallerName: Lab Caller
|
||||
QueueCode: voice_lab
|
||||
Extension: 7000
|
||||
Context: from-softphones
|
||||
Direction: inbound
|
||||
```
|
||||
|
||||
## `MVPCCCallEnded`
|
||||
|
||||
Required fields:
|
||||
|
||||
- `CallID`
|
||||
- `LinkedID`
|
||||
- `HangupCause`
|
||||
- `DurationSeconds`
|
||||
|
||||
## `MVPCCRecordingReady`
|
||||
|
||||
Required fields:
|
||||
|
||||
- `CallID`
|
||||
- `LinkedID`
|
||||
- `RemotePath`
|
||||
- `FileName`
|
||||
- `MimeType`
|
||||
- `DurationSeconds`
|
||||
|
||||
Notes:
|
||||
|
||||
- `RemotePath` is the absolute path on the Asterisk Linux VM.
|
||||
- The platform downloads the file over SFTP, then uploads it into `recording-service`.
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "call.recording.ready.json",
|
||||
"title": "call.recording.ready",
|
||||
"type": "object",
|
||||
"required": ["event_type", "call_id", "payload"],
|
||||
"properties": {
|
||||
"event_type": {
|
||||
"const": "recording.ready"
|
||||
},
|
||||
"call_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"interaction_id": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": ["source_path"],
|
||||
"properties": {
|
||||
"source_path": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"file_name": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"mime_type": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"duration_seconds": {
|
||||
"type": ["integer", "null"],
|
||||
"minimum": 0
|
||||
},
|
||||
"recorded_at": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://mvp-cc.local/docs/architecture/event-envelope.json",
|
||||
"title": "EventEnvelope",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"event_id",
|
||||
"event_type",
|
||||
"event_version",
|
||||
"occurred_at",
|
||||
"producer",
|
||||
"entity_type",
|
||||
"entity_id",
|
||||
"routing_key",
|
||||
"payload"
|
||||
],
|
||||
"properties": {
|
||||
"event_id": {"type": "string", "minLength": 1},
|
||||
"event_type": {"type": "string", "minLength": 1},
|
||||
"event_version": {"type": "integer", "minimum": 1},
|
||||
"occurred_at": {"type": "string", "format": "date-time"},
|
||||
"producer": {"type": "string", "minLength": 1},
|
||||
"entity_type": {"type": "string", "minLength": 1},
|
||||
"entity_id": {"type": "string", "minLength": 1},
|
||||
"correlation_id": {"type": ["string", "null"]},
|
||||
"routing_key": {"type": "string", "minLength": 1},
|
||||
"payload": {"type": "object"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -1,37 +1,41 @@
|
||||
# Event Schema Index
|
||||
# Контракты Событий (Event Schemas)
|
||||
|
||||
- `interaction.created.json`
|
||||
- `interaction.assigned.json`
|
||||
- `interaction.escalated.json`
|
||||
- `interaction.closed.json`
|
||||
- `agent.state.changed.json`
|
||||
- `call.recording.ready.json`
|
||||
- `call.connected.json`
|
||||
- `call.transferred.json`
|
||||
- `ivr.completed.json`
|
||||
Платформа контакт-центра использует событийно-ориентированную (Event-Driven) архитектуру поверх брокера сообщений **RabbitMQ** (с использованием паттерна Transactional Outbox). Все события (сообщения) следуют подходу "Contract-First" и описаны JSON-схемами.
|
||||
|
||||
Each file is a JSON Schema (draft 2020-12) and can be used as a validation contract between services.
|
||||
Исходники всех актуальных JSON-схем хранятся в директории `contracts/events/`.
|
||||
|
||||
Track 8 introduces a canonical transport envelope for bus-delivered events:
|
||||
## Список доменных событий (Domain Events)
|
||||
|
||||
- `docs/architecture/event-envelope.json`
|
||||
### Жизненный цикл звоноков (Telephony & Media Events)
|
||||
Шлюз телефонии (Asterisk Bridge Service) инфраструктура генерирует события о состоянии вызовов в реальном времени.
|
||||
|
||||
Payload-specific schemas under `contracts/events` remain the authoritative contracts for the
|
||||
domain payload inside that envelope.
|
||||
- `call.connected.json` — Выстреливает, когда звонок успешно сопряжен между абонентом и конечной точкой (оператором или ИИ-помощником).
|
||||
- `call.transferred.json` — Срабатывает при физическом переключении звонка (например, слепой трансфер от оператора к оператору, или Handoff от ИИ к живой очереди).
|
||||
- `call.recording.ready.json` — Ивент о том, что аудиозапись звонка завершена, обработана и скопирована в хранилище (управляется `recording-service`).
|
||||
- `ivr.completed.json` — Генерируется сервисом `ivr-service`, когда абонент завершил обход голосового меню (нажал необходимые DTMF цифры) и готов к маршрутизации.
|
||||
|
||||
Track 4 now provides the concrete schema file for the recording import placeholder:
|
||||
### Жизненный цикл обращений (Interaction Events)
|
||||
Отвечают за высокоуровневую бизнес-логику тикетов и многоканальных чатов. В основном генерируются в `interaction-service`.
|
||||
|
||||
- `docs/architecture/call.recording.ready.json`
|
||||
- `interaction.created.json` — В систему поступило новое обращение (клиент позвонил, написал в Telegram/Webchat или Email). Содержит метаданные канала.
|
||||
- `interaction.assigned.json` — Обращение захвачено живым агентом (или назначено routing-движком).
|
||||
- `interaction.escalated.json` — Агент или ИИ эскалировал тикет на уровень выше (например, на вторую линию поддержки - L2, или пометил тикет как критичный).
|
||||
- `interaction.closed.json` — Диалог или звонок успешно завершен, подведены итоги работы.
|
||||
|
||||
Track 5 adds the concrete schema for IVR completion:
|
||||
### Состояния операторов (Agent State Events)
|
||||
- `agent.state.changed.json` — Транслирует изменения статуса сотрудника ("Готов", "Перерыв", "В разговоре"). Используется в `supervisor-service` для отрисовки Dashboard в реальном времени, а также потребляется `routing-service` для распределения звонков по свободным операторам.
|
||||
|
||||
- `docs/architecture/ivr.completed.json`
|
||||
## Структура Конверта (Event Envelope)
|
||||
Любое доменное сообщение оборачивается в стандартизированный JSON-конверт для успешного трансфера через шину `event-bus-service`:
|
||||
|
||||
Track 9 adds the dialplan-to-platform `UserEvent` contract reference:
|
||||
|
||||
- `docs/architecture/asterisk-user-events.md`
|
||||
|
||||
Track 11 adds additive voice lifecycle event types produced by call-control flow:
|
||||
|
||||
- `call.connected` (operator connected to customer)
|
||||
- `call.transferred` (blind transfer requested by operator/admin)
|
||||
```json
|
||||
{
|
||||
"event_id": "8f39b1a0-54f3-...",
|
||||
"event_type": "call.connected",
|
||||
"timestamp": "2026-04-06T12:00:00Z",
|
||||
"source_service": "asterisk-bridge-service",
|
||||
"payload": {
|
||||
// Внутреннее содержимое строго по одной из JSON-схем, описанных выше
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://mvp-cc.local/contracts/events/ivr.completed.json",
|
||||
"title": "ivr.completed",
|
||||
"type": "object",
|
||||
"required": ["event_type", "call_id", "payload"],
|
||||
"properties": {
|
||||
"event_type": {
|
||||
"const": "ivr.completed"
|
||||
},
|
||||
"call_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"interaction_id": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": ["session_id", "flow_id", "outcome_code", "resolved_queue_id", "digits", "terminal_node_id"],
|
||||
"properties": {
|
||||
"session_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"flow_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"outcome_code": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"resolved_queue_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"digits": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]$"
|
||||
}
|
||||
},
|
||||
"terminal_node_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -1,25 +1,53 @@
|
||||
# Architecture Overview
|
||||
# Обзор Архитектуры Контакт-Центра (MVP)
|
||||
|
||||
## Services
|
||||
- api-gateway
|
||||
- auth-service
|
||||
- audit-service
|
||||
- customer-service
|
||||
- interaction-service
|
||||
- routing-service
|
||||
- voice-adapter-service
|
||||
- telegram-adapter-service
|
||||
- kb-service
|
||||
- reporting-service
|
||||
- supervisor-service
|
||||
Контакт-центр спроектирован на базе микросервисной архитектуры с использованием событийно-ориентированного подхода (Event-Driven Architecture). Описываемая платформа поддерживает омниканальные коммуникации (Голос, Telegram, Email, Webchat, WhatsApp) с глубокой интеграцией AI-операторов (Voice AI V1, Telegram AI) и полноценной системой маршрутизации.
|
||||
|
||||
## Data and event strategy
|
||||
- All services use SQL storage via `DATABASE_URL`
|
||||
- Default local mode: SQLite (`.data/mvp_cc.db`)
|
||||
- Target mode: PostgreSQL (supported in `deployment/docker-compose.yml`)
|
||||
- Contract-first API and JSON event schema artifacts
|
||||
- Event contracts are fixed and ready for message bus integration (RabbitMQ/Kafka) in next phase
|
||||
## 1. Основные компоненты (Микросервисы)
|
||||
|
||||
## Deployment
|
||||
- Local: Docker Compose
|
||||
- On-prem: Kubernetes manifests and Helm chart
|
||||
Архитектура состоит из множества независимых сервисов, написанных на Python (FastAPI):
|
||||
|
||||
### Базовые и инфраструктурные сервисы
|
||||
- **`api-gateway`** — Единая точка входа для клиентских интерфейсов (UI оператора/супервизора/админа).
|
||||
- **`auth-service`** — Сервис аутентификации и RBAC. Поддерживает OIDC (Keycloak) и авторизацию по `APP_TOKEN_SECRET`.
|
||||
- **`audit-service`** — Служба записи логов аудита системных действий (подключена к шине событий).
|
||||
- **`event-bus-service`** — Асинхронная шина событий. Реализует паттерн Transactional Outbox/Inbox поверх брокера **RabbitMQ** для надежной доставки сообщений между сервисами.
|
||||
|
||||
### Клиентоцентричные сервисы и роутинг
|
||||
- **`customer-service`** — Хранит профили клиентов. Управляет связыванием номеров телефонов и Telegram/Webchat-аккаунтов в единый профиль (`customer_external_identities`).
|
||||
- **`interaction-service`** — Канонический источник истины об "обращениях" (Interactions). Хранит таймлайн общения клиента и статусы тикетов.
|
||||
- **`routing-service`** — Умная маршрутизация: оценка занятости сотрудников, скилл-бейзд роутинг, распределение по очередям.
|
||||
|
||||
### Телефония и Медиа каналы (Adapters)
|
||||
- **`asterisk-bridge-service`** — Главный мост с Asterisk телефонией (перехват AMI-событий с префиксом `MVPCC`). Отслеживает звонки, трансферы и командует Asterisk'ом.
|
||||
- **`ivr-service`** — Сервис голосового меню. Настраивает IVR потоки (flows) через интерфейс администратора и обрабатывает DTMF клики.
|
||||
- **`recording-service`** — Сервис управления записями звонков. Позволяет скачивать (SFTP) и локально управлять аудиофайлами звонков.
|
||||
- **`voice-adapter-service`** — Сохраняет события жизненного цикла звонков в БД.
|
||||
- **`telegram-adapter-service`** / **`email_adapter_service`** / **`webchat_adapter_service`** / **`whatsapp_adapter_service`** — Адаптеры интеграции с внешними цифровыми каналами (мессенджерами и веб-чатами).
|
||||
|
||||
### Интеллект и База Знаний (AI & KB)
|
||||
- **`kb-service`** — Локальная База Знаний. Содержит категории, статьи и обеспечивает быстрый поиск.
|
||||
- **`ai_orchestrator_service`** — Детерминированный мозг ИИ. Принимает решения о генерации ответа или передаче диалога (Handoff) оператору на основе контекста и бизнес-правил.
|
||||
- **`ai_voice_runtime_service`** — Реал-тайм прослойка аудио-моста (Speech-to-Text / Text-to-Speech) для голосового робота. Обрабатывает перебивания (barge-in) в реальном времени.
|
||||
|
||||
### Аналитика, Отчетность и Надзор
|
||||
- **`reporting-service`** — Агрегация KPI (Service Level, ASA, AHT, Abandon Rate, FCR) в разрезе каналов и агентов.
|
||||
- **`supervisor-service`** — "Живой" мониторинг контакт-центра (статусы агентов, метрики, прослушка записей).
|
||||
|
||||
## 2. Пользовательские Интерфейсы (UI Shells)
|
||||
|
||||
Проект предоставляет несколько разделенных рабочих сред:
|
||||
- `/operator` — Единое окно оператора (Обработка звонков: «Взять в работу», «Трансфер», «Сброс», переписка Telegram/Webchat). ИИ-сводки по звонкам.
|
||||
- `/supervisor` — Окно супервизора (Мониторинг очередей, статусов агентов, поиск и прослушивание записей звонков из `recording-service`).
|
||||
- `/admin` — Администрирование (Назначение ролей, управление очередями, конфигурация IVR деревьев, превью маршрутизации).
|
||||
- `/analyst` — Выгрузки и детальная панель исторических KPI и отчетов.
|
||||
|
||||
## 3. Стратегия Данных и Развертывание
|
||||
|
||||
1. **Хранение данных:** Каждый микросервис спроектирован так, чтобы иметь изолированную логическую базу.
|
||||
- По умолчанию используется локальный **SQLite**.
|
||||
- Для K8s Scale/Продакшена используется **PostgreSQL** DB Pooling (настраивается через переменные `DB_POOL_SIZE`, `DB_MAX_OVERFLOW`).
|
||||
- Исходным механизмом миграций является `Alembic` (скрипт `scripts/migrate_core_db.py`).
|
||||
2. **Инфраструктура / Развертывание:**
|
||||
- Разработка ведется через локальные Docker Compose конфигурации (`docker-compose.yml`, `docker-compose.server.yml`). Готов набор PS/Bash скриптов в директории `scripts/` (например, `scripts/prepare_demo.ps1`).
|
||||
- Для серверов и Enterprise масштабов написаны Kubernetes Helm Chart (`deployment/helm`) и K8s манифесты.
|
||||
- Шина **RabbitMQ** используется как канонический брокер сообщений для `event_outbox`/`event_inbox` логики обмена.
|
||||
|
||||
@@ -1,744 +0,0 @@
|
||||
# Voice AI V1 Technical Design
|
||||
|
||||
Status: `Proposed`
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Add an AI-first voice operator on top of the accepted Asterisk voice baseline without replacing the current telephony stack.
|
||||
|
||||
Voice AI V1 must:
|
||||
|
||||
- answer selected inbound calls before a human joins;
|
||||
- reuse the existing `interaction`, `customer`, `KB`, `queue`, and operator UI model;
|
||||
- keep Asterisk as the system of record for call lifecycle, transfer, recording, SIP/WebRTC, and queue ownership;
|
||||
- store business state in platform tables, not only inside model context;
|
||||
- support deterministic AI-to-human handoff into the existing human queue.
|
||||
|
||||
## 2. Non-goals
|
||||
|
||||
Voice AI V1 does not:
|
||||
|
||||
- replace Asterisk, AMI, dialplan, queueing, SIP/WebRTC, or recording;
|
||||
- introduce a separate AI telephony platform or second source of truth for calls;
|
||||
- push full CRM history, full call transcript, or raw recordings into every model turn;
|
||||
- introduce a multi-agent swarm;
|
||||
- add attended transfer, conference, or full human-to-AI return in the same live call.
|
||||
|
||||
`return-to-ai` after human takeover is intentionally deferred to V2. It requires a second telephony redirect and fresh media re-attachment, which is riskier than AI-first plus human handoff.
|
||||
|
||||
## 3. Existing Baseline We Build On
|
||||
|
||||
The current repo already has the pieces needed for Voice AI V1:
|
||||
|
||||
- `asterisk_bridge_service` owns AMI ingestion, live call tracking, claim/hangup/transfer, and recording import.
|
||||
- `voice_adapter_service` stores telephony lifecycle events.
|
||||
- `interaction_service` owns the canonical interaction lifecycle.
|
||||
- `customer_service` plus `customer_external_identities` already provide cross-channel identity linking.
|
||||
- `ai_orchestrator_service` already implements the Telegram AI pattern:
|
||||
- deterministic orchestration;
|
||||
- explicit AI disclosure;
|
||||
- `ai_sessions` and `ai_turns`;
|
||||
- AI-to-human handoff;
|
||||
- source-of-truth separation between channel service and AI service.
|
||||
- `/operator` already has a hybrid Telegram AI UX with AI badges and AI summary cards.
|
||||
|
||||
Voice AI V1 should copy the Telegram AI service boundary pattern, not the Telegram channel model itself.
|
||||
|
||||
## 4. Target Architecture
|
||||
|
||||
### 4.1 Service responsibilities
|
||||
|
||||
| Service | Role in Voice AI V1 |
|
||||
| --- | --- |
|
||||
| `asterisk_bridge_service` | Keeps telephony truth, selects AI path for eligible calls, starts/stops AI runtime sessions, performs AI-to-human redirect, exposes operator-facing voice AI summary. |
|
||||
| `ai_voice_runtime_service` | New realtime service. Owns media session state, ASR/TTS streaming, turn detection, barge-in, runtime latency budget, and handoff requests. |
|
||||
| `ai_orchestrator_service` | Reused and extended. Owns deterministic voice decisioning, filtered context assembly, KB/tool usage, safe business policies, and AI summary generation. |
|
||||
| `interaction_service` | Remains the owner of interaction status and timeline; gains an internal timeline append endpoint for additive AI events. |
|
||||
| `voice_adapter_service` | Stays the owner of telephony event persistence only. It does not become the AI runtime. |
|
||||
| `ui/operator` | Reuses the current live call and popup flow; adds voice AI badges and handoff summary, but no separate `AI telephony` workspace. |
|
||||
|
||||
### 4.2 High-level component view
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Asterisk<br/>queue, redirect, recording, SIP/WebRTC"] --> B["asterisk_bridge_service<br/>call truth + control plane"]
|
||||
B --> C["ai_voice_runtime_service<br/>media bridge + ASR/TTS + barge-in"]
|
||||
C --> D["ai_orchestrator_service<br/>policy + tools + summaries"]
|
||||
D --> E["customer_service / shared DB<br/>customer profile + memory"]
|
||||
D --> F["interaction_service<br/>interaction status + timeline"]
|
||||
D --> G["kb_service<br/>KB lookup"]
|
||||
B --> H["operator UI<br/>live call popup + voice AI summary"]
|
||||
```
|
||||
|
||||
### 4.3 Source-of-truth rule
|
||||
|
||||
- Telephony truth stays in `AsteriskCallLinkRow`, AMI events, queue redirect, and recording flow.
|
||||
- AI truth stays in `voice_ai_sessions`, `ai_sessions`, `ai_turns`, and transcript rows.
|
||||
- Interaction truth stays in `Interaction` plus `InteractionTimeline`.
|
||||
- Customer truth stays in `Customer` plus `CustomerExternalIdentity`.
|
||||
|
||||
No single prompt becomes the long-term state container.
|
||||
|
||||
## 5. Call Flow
|
||||
|
||||
### 5.1 AI-first inbound voice flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Asterisk
|
||||
participant B as asterisk_bridge_service
|
||||
participant R as ai_voice_runtime_service
|
||||
participant O as ai_orchestrator_service
|
||||
participant I as interaction_service
|
||||
|
||||
A->>B: MVPCCCallStarted
|
||||
B->>I: create / reuse interaction
|
||||
B->>B: create or update AsteriskCallLinkRow
|
||||
B->>R: POST /internal/voice-ai/sessions
|
||||
R->>O: POST /ai/voice/sessions/{id}/start
|
||||
R->>A: play AI disclosure + greeting
|
||||
A->>R: caller audio stream
|
||||
R->>O: POST /ai/voice/sessions/{id}/turns
|
||||
O-->>R: reply plan or handoff decision
|
||||
R->>A: TTS playback
|
||||
```
|
||||
|
||||
Detailed steps:
|
||||
|
||||
1. `asterisk_bridge_service` receives the existing `MVPCCCallStarted` event.
|
||||
2. It keeps the current behavior of creating or resolving `interaction_id` and `AsteriskCallLinkRow`.
|
||||
3. It evaluates whether this queue or IVR outcome should go to AI-first mode.
|
||||
4. If not AI-eligible, the current human flow stays unchanged.
|
||||
5. If AI-eligible:
|
||||
- create `voice_ai_session`;
|
||||
- create or link generic `ai_session` with `channel="voice"`;
|
||||
- mark the call row as AI-owned;
|
||||
- start a runtime control session in `ai_voice_runtime_service`;
|
||||
- attach the customer leg to the AI media bridge.
|
||||
6. `ai_voice_runtime_service` plays a short disclosure and greeting.
|
||||
7. Finalized caller utterances are sent to `ai_orchestrator_service`.
|
||||
8. The orchestrator loads filtered business context, runs deterministic policy and KB/tool lookup, and returns a structured decision.
|
||||
9. The runtime either:
|
||||
- plays TTS back to the caller; or
|
||||
- requests handoff to a human queue.
|
||||
|
||||
### 5.2 AI-to-human handoff
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R as ai_voice_runtime_service
|
||||
participant O as ai_orchestrator_service
|
||||
participant B as asterisk_bridge_service
|
||||
participant A as Asterisk
|
||||
participant U as Operator UI
|
||||
|
||||
R->>O: POST voice turn
|
||||
O-->>R: needs_handoff=true + reason + summary
|
||||
R->>B: POST /internal/voice-ai/calls/{call_id}/handoff
|
||||
B->>B: mark ai_state=handoff_required
|
||||
B->>A: redirect caller leg to existing human queue
|
||||
A->>U: normal operator incoming call path
|
||||
U->>B: GET /asterisk/live-calls/{call_id}/ai-summary
|
||||
B-->>U: AI handoff summary
|
||||
```
|
||||
|
||||
Handoff rules:
|
||||
|
||||
- handoff happens only through the existing Asterisk queue path;
|
||||
- the AI runtime never calls an operator directly and never becomes a second queueing owner;
|
||||
- `interaction_id` stays the same through AI and human phases;
|
||||
- the operator receives a concise AI summary before or during claim.
|
||||
|
||||
### 5.3 Call end
|
||||
|
||||
- `MVPCCCallEnded` and `MVPCCRecordingReady` continue to come from Asterisk through the existing bridge.
|
||||
- The bridge informs the runtime session that telephony ended.
|
||||
- The runtime closes `voice_ai_session`.
|
||||
- The orchestrator writes the final AI summary and marks `ai_session` closed.
|
||||
- The interaction timeline gets final AI events if they were not written yet.
|
||||
|
||||
## 6. Queue Selection and AI Eligibility
|
||||
|
||||
Voice AI V1 should start with config-driven AI eligibility, not a new admin UI.
|
||||
|
||||
Recommended V1 config:
|
||||
|
||||
`AI_VOICE_QUEUE_CONFIG_JSON`
|
||||
|
||||
Example shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"voice_lab": {
|
||||
"mode": "ai_first",
|
||||
"agent_profile": "voice_support",
|
||||
"handoff_queue_code": "voice_lab",
|
||||
"language": "ru"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Why config-first:
|
||||
|
||||
- it avoids touching the accepted routing UI and queue schema in the first step;
|
||||
- it keeps rollout reversible per queue;
|
||||
- it matches the current repo style where telephony behavior is still largely env-driven.
|
||||
|
||||
The longer-term path can move this policy into routing/admin later.
|
||||
|
||||
## 7. Data Model
|
||||
|
||||
## 7.1 Reused tables
|
||||
|
||||
These tables stay authoritative and should be reused:
|
||||
|
||||
- `interactions`
|
||||
- `interaction_timelines`
|
||||
- `customer_external_identities`
|
||||
- `asterisk_call_links`
|
||||
- `voice_events`
|
||||
- `call_recordings`
|
||||
- `ai_sessions`
|
||||
- `ai_turns`
|
||||
|
||||
Voice customer linking should reuse `customer_external_identities` with:
|
||||
|
||||
- `channel = "voice"`
|
||||
- `external_subject = normalized caller number`
|
||||
|
||||
No new identity table is needed.
|
||||
|
||||
## 7.2 New table: `voice_ai_sessions`
|
||||
|
||||
This table stores runtime state that does not fit cleanly into generic `ai_sessions`.
|
||||
|
||||
Suggested columns:
|
||||
|
||||
| Column | Type | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `id` | integer pk | Internal row id |
|
||||
| `session_id` | string unique | Public runtime session id |
|
||||
| `call_id` | string unique index | Current Asterisk customer-leg call id |
|
||||
| `linked_id` | string index | Asterisk linked id for recovery if call id changes |
|
||||
| `interaction_id` | string index | Shared interaction |
|
||||
| `customer_id` | string index nullable | Linked customer |
|
||||
| `queue_id` | string index | Original platform queue |
|
||||
| `ai_session_id` | string index | Generic AI session link |
|
||||
| `agent_profile` | string index | Voice policy profile |
|
||||
| `status` | string index | `queued`, `greeting`, `listening`, `thinking`, `speaking`, `handoff_requested`, `human_owned`, `completed`, `error` |
|
||||
| `language` | string index nullable | Current active language |
|
||||
| `asr_provider` | string nullable | Selected ASR backend |
|
||||
| `tts_provider` | string nullable | Selected TTS backend |
|
||||
| `handoff_reason` | text nullable | Last handoff reason |
|
||||
| `handoff_target_queue_id` | string nullable | Human target queue |
|
||||
| `disclosure_played_at` | string nullable | When AI disclosure was first spoken |
|
||||
| `last_user_utterance_at` | string nullable | Latest finalized caller speech |
|
||||
| `last_ai_reply_at` | string nullable | Latest completed AI reply |
|
||||
| `started_at` | string index | Session start |
|
||||
| `updated_at` | string index | Last state update |
|
||||
| `ended_at` | string index nullable | Session end |
|
||||
|
||||
## 7.3 New table: `voice_transcript_segments`
|
||||
|
||||
This table stores finalized voice transcript units separately from generic AI turns.
|
||||
|
||||
Suggested columns:
|
||||
|
||||
| Column | Type | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `id` | integer pk | Internal row id |
|
||||
| `segment_id` | string unique | Public segment id |
|
||||
| `session_id` | string index | `voice_ai_sessions.session_id` |
|
||||
| `call_id` | string index | Voice call correlation |
|
||||
| `interaction_id` | string index | Interaction correlation |
|
||||
| `speaker` | string index | `caller`, `assistant`, `system`, `operator` |
|
||||
| `source_type` | string index | `asr`, `tts`, `handoff_summary`, `system` |
|
||||
| `sequence_no` | integer index | Ordered segment number |
|
||||
| `text` | text | Final transcript text |
|
||||
| `confidence` | number nullable | ASR confidence when applicable |
|
||||
| `is_final` | boolean | Finalized transcript only in V1, but keep the flag for future partials |
|
||||
| `barge_in_interrupted` | boolean | Whether the assistant segment was interrupted |
|
||||
| `payload_json` | text | Provider metadata, timestamps, tool refs |
|
||||
| `created_at` | string index | Write time |
|
||||
|
||||
V1 should store finalized transcript segments only. Partial ASR events can stay in memory inside the runtime.
|
||||
|
||||
## 7.4 Additive columns on `asterisk_call_links`
|
||||
|
||||
This mirrors the Telegram pattern where the channel source-of-truth row also exposes AI status.
|
||||
|
||||
Add:
|
||||
|
||||
- `voice_session_id VARCHAR(64) NULL`
|
||||
- `ai_session_id VARCHAR(64) NULL`
|
||||
- `ai_state VARCHAR(32) NULL`
|
||||
- `ai_handoff_reason TEXT NULL`
|
||||
- `ai_last_model_at VARCHAR(64) NULL`
|
||||
|
||||
Suggested `ai_state` values:
|
||||
|
||||
- `queued`
|
||||
- `greeting`
|
||||
- `listening`
|
||||
- `thinking`
|
||||
- `active`
|
||||
- `handoff_required`
|
||||
- `human_owned`
|
||||
- `closed`
|
||||
- `error`
|
||||
|
||||
This lets `/asterisk/live-calls` and `/asterisk/recent-calls` drive UI chips directly without extra joins on every poll.
|
||||
|
||||
## 7.5 Additive column on `ai_sessions`
|
||||
|
||||
Add:
|
||||
|
||||
- `call_id VARCHAR(128) NULL`
|
||||
|
||||
Reason:
|
||||
|
||||
- Telegram already uses `thread_id`;
|
||||
- voice needs a direct telephony key for fast lookup and summary generation;
|
||||
- this keeps `ai_sessions` truly cross-channel instead of Telegram-shaped.
|
||||
|
||||
## 7.6 Reuse of `ai_turns`
|
||||
|
||||
Reuse `ai_turns` for:
|
||||
|
||||
- finalized user turn seen by the orchestrator;
|
||||
- model reply plan;
|
||||
- tool invocation results;
|
||||
- summary turn written during handoff or closure.
|
||||
|
||||
Do not reuse `ai_jobs` for voice V1. Telegram jobs are thread-triggered and synchronous voice turn processing does not need that queue model.
|
||||
|
||||
## 8. API Design
|
||||
|
||||
## 8.1 `asterisk_bridge_service` -> `ai_voice_runtime_service`
|
||||
|
||||
New internal control-plane endpoints:
|
||||
|
||||
### `POST /internal/voice-ai/sessions`
|
||||
|
||||
Starts a runtime session for an already accepted telephony call.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"call_id": "1740912000.12",
|
||||
"linked_id": "1740912000.12",
|
||||
"interaction_id": "int_...",
|
||||
"queue_id": "que_...",
|
||||
"caller_number": "+7701...",
|
||||
"caller_name": "Lab Caller",
|
||||
"agent_profile": "voice_support",
|
||||
"language_hint": "ru",
|
||||
"handoff_queue_id": "que_...",
|
||||
"metadata": {
|
||||
"queue_code": "voice_lab",
|
||||
"direction": "inbound"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"voice_session_id": "avs_...",
|
||||
"ai_session_id": "ais_...",
|
||||
"status": "queued"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /internal/voice-ai/sessions/{session_id}/telephony-events`
|
||||
|
||||
Bridge notifies runtime about:
|
||||
|
||||
- `call.connected`
|
||||
- `call.ended`
|
||||
- `recording.ready`
|
||||
- `operator.connected`
|
||||
|
||||
This keeps telephony truth in the bridge while runtime stays current.
|
||||
|
||||
## 8.2 `ai_voice_runtime_service` -> `ai_orchestrator_service`
|
||||
|
||||
### `POST /ai/voice/sessions/{session_id}/start`
|
||||
|
||||
Creates or reopens the generic AI session and returns greeting policy:
|
||||
|
||||
```json
|
||||
{
|
||||
"voice_session_id": "avs_...",
|
||||
"call_id": "1740912000.12",
|
||||
"interaction_id": "int_...",
|
||||
"customer_id": "cus_...",
|
||||
"language_hint": "ru",
|
||||
"agent_profile": "voice_support"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "ais_...",
|
||||
"language": "ru",
|
||||
"greeting_text": "Здравствуйте. Я AI-оператор компании...",
|
||||
"disclosure_required": true
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /ai/voice/sessions/{session_id}/turns`
|
||||
|
||||
Main deterministic decision endpoint.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"voice_session_id": "avs_...",
|
||||
"call_id": "1740912000.12",
|
||||
"interaction_id": "int_...",
|
||||
"transcript_text": "Хочу узнать статус заявки",
|
||||
"language": "ru",
|
||||
"sequence_no": 3,
|
||||
"barge_in": false,
|
||||
"metadata": {
|
||||
"turn_duration_ms": 4200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "ru",
|
||||
"intent": "status_check",
|
||||
"reply_text": "Я AI-оператор компании. Проверяю данные по обращению...",
|
||||
"confidence": 0.83,
|
||||
"needs_handoff": false,
|
||||
"handoff_reason": null,
|
||||
"case_action": "keep_open",
|
||||
"kb_refs": ["art_..."],
|
||||
"summary_text": "Клиент уточняет статус заявки.",
|
||||
"model": "gpt-4o-mini",
|
||||
"latency_ms": 780
|
||||
}
|
||||
```
|
||||
|
||||
`case_action` should stay aligned with the existing Telegram pattern:
|
||||
|
||||
- `none`
|
||||
- `keep_open`
|
||||
- `close`
|
||||
- `escalate`
|
||||
|
||||
### `POST /ai/voice/sessions/{session_id}/close`
|
||||
|
||||
Finalizes AI state and writes the terminal summary.
|
||||
|
||||
## 8.3 `ai_voice_runtime_service` -> `asterisk_bridge_service`
|
||||
|
||||
### `POST /internal/voice-ai/calls/{call_id}/handoff`
|
||||
|
||||
Requests redirect of the current customer leg into the existing human queue.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"voice_session_id": "avs_...",
|
||||
"ai_session_id": "ais_...",
|
||||
"interaction_id": "int_...",
|
||||
"target_queue_id": "que_...",
|
||||
"reason": "Нужен человек для чувствительного запроса.",
|
||||
"summary": {
|
||||
"customer_request_text": "Клиент просит изменить договор",
|
||||
"ai_outcome_text": "AI собрал контекст и не выполнял чувствительное действие",
|
||||
"recommended_next_step": "Проверить договор и продолжить вручную"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- bridge validates the call is still active;
|
||||
- bridge updates `ai_state` to `handoff_required`;
|
||||
- bridge appends timeline `ai.handoff_requested`;
|
||||
- bridge redirects the customer leg into the configured human queue;
|
||||
- when the normal operator-connected flow happens, the same call row becomes `human_owned`.
|
||||
|
||||
This endpoint is internal-only and trusted for service actors such as `svc:ai-voice-runtime`.
|
||||
|
||||
## 8.4 `interaction_service`
|
||||
|
||||
Add one internal endpoint:
|
||||
|
||||
### `POST /interactions/{interaction_id}/timeline`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "ai.reply_generated",
|
||||
"metadata": {
|
||||
"call_id": "1740912000.12",
|
||||
"voice_session_id": "avs_...",
|
||||
"ai_session_id": "ais_..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Why add this now:
|
||||
|
||||
- Voice AI should not write cross-service timeline rows by reaching into another service's DB contract ad hoc;
|
||||
- the same endpoint can later be reused by Telegram AI without changing its current behavior immediately;
|
||||
- it makes the AI event contract explicit.
|
||||
|
||||
Required Voice AI timeline actions:
|
||||
|
||||
- `ai.session_started`
|
||||
- `ai.reply_generated`
|
||||
- `ai.handoff_requested`
|
||||
- `ai.handoff_completed`
|
||||
- `ai.error`
|
||||
|
||||
Existing interaction endpoints remain reused as-is:
|
||||
|
||||
- `PATCH /interactions/{id}/status`
|
||||
- `POST /interactions/{id}/escalate`
|
||||
- `PATCH /interactions/{id}/assign`
|
||||
|
||||
## 8.5 Operator-facing read APIs
|
||||
|
||||
Extend `asterisk_bridge_service` output:
|
||||
|
||||
### `GET /asterisk/live-calls`
|
||||
|
||||
Add to each row:
|
||||
|
||||
- `voice_session_id`
|
||||
- `ai_session_id`
|
||||
- `ai_state`
|
||||
- `ai_handoff_reason`
|
||||
- `ai_last_model_at`
|
||||
|
||||
### `GET /asterisk/recent-calls`
|
||||
|
||||
Expose the same additive AI fields.
|
||||
|
||||
### `GET /asterisk/live-calls/{call_id}/ai-summary`
|
||||
|
||||
Return a summary shape intentionally aligned with Telegram:
|
||||
|
||||
```json
|
||||
{
|
||||
"call_id": "1740912000.12",
|
||||
"session_id": "ais_...",
|
||||
"voice_session_id": "avs_...",
|
||||
"status_label": "AI передал звонок оператору",
|
||||
"status_tone": "handoff",
|
||||
"customer_request_text": "Клиент хочет узнать статус обращения и изменить способ оплаты",
|
||||
"ai_outcome_text": "AI собрал контекст и объяснил рамки, но не выполнил чувствительное действие",
|
||||
"handoff_reason": "Запрос требует человека и проверки вручную",
|
||||
"recommended_next_step": "Проверить карточку обращения и продолжить звонок вручную",
|
||||
"generated_at": "2026-03-09T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
This should be produced from `voice_ai_sessions`, `ai_turns`, and the latest transcript segments.
|
||||
|
||||
## 9. Context Filtering Rules
|
||||
|
||||
Voice AI must not send the whole call history into the model each turn.
|
||||
|
||||
For every voice turn, `ai_orchestrator_service` should assemble a filtered context from:
|
||||
|
||||
- customer profile:
|
||||
- `customer_id`
|
||||
- display name
|
||||
- preferred phone
|
||||
- tags
|
||||
- customer memory:
|
||||
- recent resolved issues
|
||||
- notable preferences
|
||||
- interaction state:
|
||||
- `interaction_id`
|
||||
- status
|
||||
- queue
|
||||
- last relevant timeline events
|
||||
- voice session state:
|
||||
- language
|
||||
- disclosure already played or not
|
||||
- previous handoff flag
|
||||
- current turn number
|
||||
- recent transcript window:
|
||||
- last `6-10` finalized segments, not the entire transcript
|
||||
- KB:
|
||||
- top `3` matched articles or fewer
|
||||
- business policy:
|
||||
- allowed actions
|
||||
- mandatory disclosure
|
||||
- sensitive-topic escalation rules
|
||||
|
||||
This matches the Telegram AI design principle already present in the repo.
|
||||
|
||||
## 10. Runtime Behavior
|
||||
|
||||
## 10.1 ASR/TTS abstraction
|
||||
|
||||
`ai_voice_runtime_service` should expose provider interfaces and start with one configured provider per environment.
|
||||
|
||||
Recommended internal modules:
|
||||
|
||||
- `providers/asr.py`
|
||||
- `providers/tts.py`
|
||||
- `session_manager.py`
|
||||
- `media_bridge.py`
|
||||
- `barge_in.py`
|
||||
|
||||
V1 supports one active ASR provider and one active TTS provider, behind interfaces. Multi-provider fallback is not required in the first version.
|
||||
|
||||
## 10.2 Barge-in
|
||||
|
||||
Barge-in is a V1 requirement because voice UX breaks if the caller cannot interrupt TTS.
|
||||
|
||||
Required behavior:
|
||||
|
||||
- while TTS is playing, incoming speech activity stops or fades out current playback;
|
||||
- interrupted assistant output is marked with `barge_in_interrupted=true` in transcript;
|
||||
- only finalized caller speech creates a new orchestrator turn;
|
||||
- if interruption happens repeatedly or ASR confidence is poor, handoff rules may trigger.
|
||||
|
||||
## 10.3 Latency budget
|
||||
|
||||
Target budget for one AI turn:
|
||||
|
||||
- end-of-utterance to finalized ASR text: `<= 600 ms`
|
||||
- orchestrator decision: `<= 900 ms`
|
||||
- first TTS audio chunk: `<= 500 ms`
|
||||
- total pause before AI speech starts: `<= 2.0 s`
|
||||
|
||||
If the runtime cannot stay within the budget repeatedly, it should prefer human handoff over a degraded long-silence experience.
|
||||
|
||||
## 11. Operator UI Integration
|
||||
|
||||
Voice AI V1 should integrate into the current operator shell, not create a second voice console.
|
||||
|
||||
### 11.1 Existing surfaces to extend
|
||||
|
||||
- browser call popup overlay;
|
||||
- `Voice debug` live/recent calls list;
|
||||
- unified customer history on `/operator`.
|
||||
|
||||
### 11.2 Required UI changes
|
||||
|
||||
1. Extend live call rows with an AI chip using the same language as Telegram:
|
||||
- `AI active`
|
||||
- `Ждёт человека`
|
||||
- `AI error`
|
||||
2. When a transferred AI-owned call reaches the operator popup:
|
||||
- load `GET /asterisk/live-calls/{call_id}/ai-summary`;
|
||||
- show a compact `Сводка AI` card above call actions;
|
||||
- keep existing `Принять в работу`, `Передать`, `Завершить` controls unchanged.
|
||||
3. Add AI metadata to customer history:
|
||||
- AI session started
|
||||
- AI handoff requested
|
||||
- AI handoff completed
|
||||
4. Do not add a separate full transcript workspace in V1.
|
||||
|
||||
### 11.3 UI behavior intentionally deferred
|
||||
|
||||
Deferred to V2:
|
||||
|
||||
- operator button `Вернуть AI` for live voice calls;
|
||||
- inline live transcript for operators during the call;
|
||||
- supervisor transcript explorer for full recordings plus transcripts.
|
||||
|
||||
## 12. Deployment and Config
|
||||
|
||||
## 12.1 New service in `docker-compose.server.yml`
|
||||
|
||||
Add:
|
||||
|
||||
- `ai-voice-runtime-service`
|
||||
|
||||
New shared env:
|
||||
|
||||
- `AI_VOICE_RUNTIME_SERVICE_URL`
|
||||
- `AI_VOICE_ENABLED`
|
||||
- `AI_VOICE_QUEUE_CONFIG_JSON`
|
||||
- `AI_VOICE_ASR_PROVIDER`
|
||||
- `AI_VOICE_TTS_PROVIDER`
|
||||
- `AI_VOICE_TTS_CACHE_ENABLED`
|
||||
- `AI_VOICE_TTS_CACHE_DIR`
|
||||
- `AI_VOICE_MAX_CONTEXT_SEGMENTS`
|
||||
- `AI_VOICE_HANDOFF_TIMEOUT_SECONDS`
|
||||
|
||||
Service auth:
|
||||
|
||||
- add trusted subject `svc:ai-voice-runtime` where internal bridge endpoints require it.
|
||||
|
||||
## 12.2 Asterisk-side change
|
||||
|
||||
The accepted human voice path stays intact.
|
||||
|
||||
Voice AI adds one new AI media bridge path in dialplan only for AI-selected queues. Preferred V1 implementation is:
|
||||
|
||||
- Asterisk dialplan redirects the customer leg into an external media bridge context dedicated to AI.
|
||||
|
||||
Exact low-level primitive should be validated in the lab:
|
||||
|
||||
- preferred: `AudioSocket` or equivalent bidirectional audio bridge;
|
||||
- fallback: narrowly scoped external-media/ARI only for AI queues.
|
||||
|
||||
The choice must keep Asterisk as the telephony owner.
|
||||
|
||||
## 13. Rollout Order
|
||||
|
||||
Recommended implementation order:
|
||||
|
||||
1. schema changes only:
|
||||
- `voice_ai_sessions`
|
||||
- `voice_transcript_segments`
|
||||
- additive AI columns on `asterisk_call_links`
|
||||
- additive `call_id` on `ai_sessions`
|
||||
2. control plane only:
|
||||
- bridge starts and closes empty runtime sessions behind `AI_VOICE_ENABLED=0`
|
||||
3. lab media path:
|
||||
- one AI-enabled lab queue
|
||||
- disclosure + greeting + ASR/TTS echo flow
|
||||
4. orchestrator integration:
|
||||
- KB lookup
|
||||
- filtered context
|
||||
- handoff decisioning
|
||||
5. operator summary:
|
||||
- popup card
|
||||
- voice debug AI chips
|
||||
6. staged pilot rollout per queue.
|
||||
|
||||
## 14. Key Risks and Open Questions
|
||||
|
||||
1. `call_id` stability during AI-to-human redirect must be validated in the Asterisk lab.
|
||||
If redirect creates a new call id, bridge recovery must switch to `linked_id` first and only then reuse `interaction_id`.
|
||||
2. The exact Asterisk media primitive must be confirmed before implementation.
|
||||
The design assumes a bidirectional bridge is available without replacing the accepted telephony baseline.
|
||||
3. Provider latency must be measured in the target environment before enabling AI-first for production queues.
|
||||
4. Sensitive actions should stay read-only in V1.
|
||||
Voice AI should use KB, customer lookup, and safe interaction updates, but not execute risky external business actions directly.
|
||||
|
||||
## 15. Summary
|
||||
|
||||
Voice AI V1 should be implemented as an additive AI layer over the accepted voice baseline:
|
||||
|
||||
- `asterisk_bridge_service` remains telephony truth and handoff executor;
|
||||
- `ai_voice_runtime_service` is the new realtime media layer;
|
||||
- `ai_orchestrator_service` is reused for deterministic business decisioning;
|
||||
- `interaction_service` remains the owner of the canonical AI timeline;
|
||||
- operator UI gets AI badges and AI handoff summary, not a new telephony product.
|
||||
|
||||
This keeps the current live voice contour intact while adding the same AI-first and human-handoff architecture that already works in Telegram.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Архитектура Голосового ИИ (Voice AI V1)
|
||||
|
||||
Этот документ описывает техническое устройство и жизненный цикл обработки входящих голосовых звонков с помощью ИИ-оператора. ИИ интегрирован поверх базовой телефонии Asterisk и не заменяет ее полностью, выступая как первая линия поддержки (L1).
|
||||
|
||||
## 1. Ключевые принципы
|
||||
|
||||
1. **AI-first**: Звонок сначала направляется ИИ, если очередь настроена соответствующим образом.
|
||||
2. **Перехват звонка (Handoff)**: Исходный звонок детерминированно переключается на человека-оператора, если ИИ не может решить проблему или если клиент явно просит человека.
|
||||
3. **Единый источник истины**: Asterisk остается мастер-системой для SIP-потока, записи звонков и маршрутизации.
|
||||
4. **Безопасность (Read-Only)**: В версии V1 ИИ работает исключительно на чтение и выдачу справочной информации. Апдейт критичных финансовых данных в базе строго запрещен для ИИ и требует переключения на человека.
|
||||
|
||||
## 2. Архитектура обработки звонка
|
||||
|
||||
Процесс обработки звонка ИИ разделен на два основных микросервиса:
|
||||
|
||||
- **`ai_voice_runtime_service` (Аудио-прослойка):** Осуществляет прием аудио-потока. Работает в Real-time. Слушает аудио (через ASR модель) и отправляет синтезированную речь обратно клиенту (через TTS модель). Также отлавливает попытки клиента перебить робота (Barge-in), мгновенно прерывая воспроизведение.
|
||||
- **`ai_orchestrator_service` (Мозг ИИ):** Не работает со звуком. Получает только готовый текстовый транскрипт (реплику). Принимает решение:
|
||||
1. Найти знания в `kb-service` (RAG).
|
||||
2. Сгенерировать текстовый ответ для клиента (который будет озвучен через TTS).
|
||||
3. Если запрос слишком сложный — отправить команду "Переключить на живого человека".
|
||||
|
||||
## 3. Флоу входящего звонка (Step-by-step)
|
||||
|
||||
1. Абонент звонит на платформу.
|
||||
2. **`asterisk-bridge-service`** улавливает событие и решает направить звонок в `voice_lab`.
|
||||
3. Поднимаются две параллельные сессии: `voice_ai_session` (отслеживание аудио) и `ai_session` (бизнес-логика).
|
||||
4. Абонент подключается к медиа-интерфейсу `ai_voice_runtime_service`. Проигрывается дисклеймер: "Здравствуйте, я голосовой помощник...".
|
||||
5. Абонент озвучивает свой вопрос.
|
||||
6. ASR распознает вопрос и отправляет текст в **`ai_orchestrator_service`**.
|
||||
7. Оркестратор смотрит историю клиента, ищет ответ в базе знаний, генерирует ответ и возвращает команду в runtime "Скажи это".
|
||||
8. Если оркестратор понимает, что нужна помощь:
|
||||
- Отправляет сигнал `needs_handoff` с детальной "Сводкой ИИ" для оператора.
|
||||
- Звонок перехватывается, и абонент слушает музыку ожидания в очереди к живым людям.
|
||||
|
||||
## 4. Интеграция с Operator UI
|
||||
|
||||
Когда звонок поступает к оператору после ИИ, оператор не видит отдельный ИИ-интерфейс. Он видит стандартную карточку звонка. Но внутри этой карточки динамически выводится:
|
||||
- **AI Summary (Сводка):** Что клиент сказал, что ИИ попытался сделать и почему перевел звонок на человека.
|
||||
- **Таймлайн:** В историю обращений клиента логгируется, что "ИИ запрашивал перевод на специалиста".
|
||||
@@ -1,8 +0,0 @@
|
||||
# Gate 1 Checklist (Foundation)
|
||||
|
||||
- [ ] CI pipeline executes tests
|
||||
- [ ] Gateway and stage 1 services expose `/health`
|
||||
- [ ] `/auth/login` and RBAC restrictions validated
|
||||
- [ ] Stage 1 OpenAPI published
|
||||
- [ ] Event schemas approved
|
||||
- [ ] Kubernetes + Helm manifests validated by lint/dry run
|
||||
@@ -1,8 +0,0 @@
|
||||
# Gate 2 Checklist (Operator Core)
|
||||
|
||||
- [ ] Create customer and interaction (voice/telegram)
|
||||
- [ ] Route and assign interaction
|
||||
- [ ] Escalate interaction and preserve timeline
|
||||
- [ ] Voice event and Telegram webhook ingestion works
|
||||
- [ ] Stage 2 OpenAPI frozen
|
||||
- [ ] Functional tests pass
|
||||
@@ -1,12 +0,0 @@
|
||||
# Gate 3 Checklist (Supervisor + Reporting + KB-lite)
|
||||
|
||||
- [x] KPI events ingested and core KPIs available
|
||||
- [x] Supervisor realtime reflects agent and queue state
|
||||
- [x] KB categories/articles/search operate by role
|
||||
- [x] CSV export works
|
||||
- [x] Stage 3 OpenAPI frozen
|
||||
- [x] Integration tests pass
|
||||
|
||||
Verification run (2026-02-26):
|
||||
- `pytest -q`
|
||||
- `python scripts/gate3_check.py --auto-start`
|
||||
@@ -1,35 +0,0 @@
|
||||
# Gate 4 Checklist (Pilot Hardening)
|
||||
|
||||
- [x] Load test target reached (100 voice + 100 digital)
|
||||
- [x] Backup and restore verified
|
||||
- [x] Security checklist completed
|
||||
- [ ] UAT signed with real operators/supervisors
|
||||
- [ ] P1/P2 defects closed
|
||||
- [x] v1 release notes and runbooks delivered
|
||||
|
||||
Verification run (2026-02-26):
|
||||
- `python scripts/gate4_check.py`
|
||||
- `pytest -q`
|
||||
|
||||
Pilot scope freeze:
|
||||
- `docs/gates/mvp-pilot-baseline.md`
|
||||
- `docs/roadmap/05-wave2-backlog.md`
|
||||
|
||||
Required manual evidence bundle before closing the last two checklist items:
|
||||
- preflight report from `docs/uat/evidence/`
|
||||
- completed session protocol from `docs/uat/session-template.md`
|
||||
- filled defect log from `docs/uat/defect-log-template.csv`
|
||||
- signed sheet from `docs/uat/signoff-template.md`
|
||||
- updated `docs/gates/p1-p2-defects.md`
|
||||
|
||||
Manual close-out sequence:
|
||||
1. Run preflight and automated dry-run to prepare the evidence pack.
|
||||
2. Execute real UAT with operators and supervisor on the frozen MVP scope.
|
||||
3. Fix only `P1` and `P2` issues, then rerun regression checks.
|
||||
4. Capture sign-off and only then mark the final two checklist items as done.
|
||||
|
||||
Notes:
|
||||
- UAT sign-off must be completed on pilot site with real operators and supervisor.
|
||||
- P1/P2 closure is finalized after pilot UAT defect intake.
|
||||
- UAT package location: `docs/uat/`.
|
||||
- Automated dry-run command: `python scripts/uat_dry_run.py --auto-start --update-defect-register`.
|
||||
@@ -1,71 +0,0 @@
|
||||
# MVP Pilot Baseline
|
||||
|
||||
This document freezes the scope for MVP pilot acceptance and separates pilot defects
|
||||
from post-MVP feature requests.
|
||||
|
||||
## Accepted Pilot Scope
|
||||
|
||||
The following capabilities are part of MVP pilot acceptance:
|
||||
|
||||
- auth login and RBAC enforcement
|
||||
- customers create/search/history shell
|
||||
- interactions create/assign/status/escalate/timeline
|
||||
- queues and routing rules
|
||||
- voice event intake
|
||||
- Telegram webhook intake
|
||||
- KB-lite categories/articles/search
|
||||
- KPI core (`SL`, `ASA`, `AHT`, `Abandon`, `FCR`) and CSV export
|
||||
- supervisor realtime API
|
||||
- current operator UI shell (`/operator`)
|
||||
|
||||
## Explicitly Out of Scope
|
||||
|
||||
The following items are not treated as MVP defects during pilot acceptance:
|
||||
|
||||
- LDAP/SSO
|
||||
- full omnichannel beyond `voice` and `Telegram`
|
||||
- standalone supervisor UI
|
||||
- standalone admin UI
|
||||
- full WFM module
|
||||
- advanced IVR builder
|
||||
- full recording suite and audio archive UX
|
||||
- full KPI catalog from the extended technical specification
|
||||
- scale target beyond MVP baseline (`100 voice + 100 digital`)
|
||||
|
||||
All out-of-scope requests must be redirected to `Wave 2` backlog:
|
||||
`docs/roadmap/05-wave2-backlog.md`.
|
||||
|
||||
## Approved Pilot Environments
|
||||
|
||||
- Primary mode: dedicated pilot gateway/environment maintained outside local auto-start.
|
||||
- Fallback mode: local rehearsal via:
|
||||
- `python scripts/uat_preflight.py --auto-start`
|
||||
- `python scripts/uat_dry_run.py --auto-start --update-defect-register`
|
||||
|
||||
## Scope Freeze Rules
|
||||
|
||||
- No breaking API changes are allowed during pilot close-out.
|
||||
- Only backward-compatible fixes are allowed for UAT remediation.
|
||||
- Only `P1` and `P2` defects are fixed inside MVP close-out.
|
||||
- `P3` and `P4` findings are logged and moved to post-MVP backlog unless they block sign-off.
|
||||
- New feature requests must not be recorded as MVP defects.
|
||||
|
||||
## Required Evidence Bundle
|
||||
|
||||
Pilot close-out is based on the following artifacts:
|
||||
|
||||
- preflight report from `docs/uat/evidence/`
|
||||
- completed session protocol
|
||||
- defect log with severity and verification steps
|
||||
- signed sign-off sheet
|
||||
- updated `docs/gates/p1-p2-defects.md`
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
The MVP pilot baseline is considered closed only when all conditions are true:
|
||||
|
||||
- all mandatory scenarios from `docs/uat/scenario-checklist.md` are executed
|
||||
- no open `P1` defects
|
||||
- no open `P2` defects
|
||||
- business and IT owners sign the UAT package
|
||||
- Gate 4 is updated to reflect final pilot closure
|
||||
@@ -1,16 +0,0 @@
|
||||
# P1/P2 Defect Register (Pilot)
|
||||
|
||||
Last update: 2026-02-27 (UAT-DRY-20260227_085446)
|
||||
|
||||
## Current Status
|
||||
- Open P1: 0
|
||||
- Open P2: 0
|
||||
- Source: `scripts/uat_dry_run.py` and automated checks
|
||||
- Latest dry-run evidence: `docs/uat/evidence/dry_run_UAT-DRY-20260227_085446/`
|
||||
|
||||
## Triage Policy
|
||||
- Only `P1` and `P2` issues belong to MVP remediation.
|
||||
- `P3` and `P4` issues move to `docs/roadmap/05-wave2-backlog.md` unless they block sign-off.
|
||||
|
||||
## Pilot Note
|
||||
- Final P1/P2 closure is confirmed only after the real-operator UAT cycle and sign-off.
|
||||
@@ -1,12 +0,0 @@
|
||||
# UAT Protocol Template (Stage 4)
|
||||
|
||||
Primary working template moved to:
|
||||
|
||||
- `docs/uat/session-template.md`
|
||||
|
||||
Additional artifacts:
|
||||
|
||||
- Scenario checklist: `docs/uat/scenario-checklist.md`
|
||||
- Defect log: `docs/uat/defect-log-template.csv`
|
||||
- Sign-off: `docs/uat/signoff-template.md`
|
||||
- Preflight evidence: `docs/uat/evidence/`
|
||||
@@ -1,37 +0,0 @@
|
||||
# KPI Definitions (MVP Core)
|
||||
|
||||
## SL (Service Level)
|
||||
`answered_within_threshold / total_interactions * 100`
|
||||
|
||||
## ASA (Average Speed of Answer)
|
||||
`sum(wait_seconds for answered) / answered`
|
||||
|
||||
## AHT (Average Handle Time)
|
||||
`sum(handle_seconds for answered) / answered`
|
||||
|
||||
## Abandon
|
||||
`abandoned / total_interactions * 100`
|
||||
|
||||
## FCR
|
||||
`resolved_first_contact / answered * 100`
|
||||
|
||||
## AnswerRate
|
||||
`answered / total_interactions * 100`
|
||||
|
||||
## WaitP95
|
||||
`p95(wait_seconds for answered)`
|
||||
|
||||
## HandleP95
|
||||
`p95(handle_seconds for answered)`
|
||||
|
||||
## Occupancy
|
||||
`sum(handle_seconds) / (sum(handle_seconds) + sum(wait_seconds)) * 100`
|
||||
|
||||
## DigitalShare
|
||||
`non_voice_interactions / total_interactions * 100`
|
||||
|
||||
## Supported Dimensions
|
||||
- `queue_id`
|
||||
- `channel`
|
||||
- `agent_id`
|
||||
- time window (`from_ts`, `to_ts`)
|
||||
@@ -1,22 +0,0 @@
|
||||
# Release Notes - v1.0.0-mvp
|
||||
|
||||
Date: 2026-02-26
|
||||
|
||||
## Included
|
||||
- Stage 1 foundation services, gateway, contracts, and deployment skeleton
|
||||
- Stage 2 operator core flows (`voice + telegram`) with interaction lifecycle
|
||||
- Stage 3 supervisor, KPI reporting, and KB-lite APIs
|
||||
- SQL persistence for all services (SQLite/PostgreSQL support)
|
||||
- Local and pilot runbooks, gate checklists, and automation scripts
|
||||
|
||||
## Hardening Pack
|
||||
- Load test script for `100 voice + 100 digital`
|
||||
- Backup/restore scripts and runbook
|
||||
- Gate 3 automation: `scripts/gate3_check.py`
|
||||
- Gate 4 automation: `scripts/gate4_check.py`
|
||||
- Security checklist: `docs/security/checklist.md`
|
||||
|
||||
## Known Limits
|
||||
- UAT sign-off by real operators/supervisors is a pilot activity
|
||||
- P1/P2 closure depends on pilot defect intake
|
||||
- LDAP planned for next wave (post-MVP)
|
||||
@@ -1,25 +0,0 @@
|
||||
# Stage 1 - Foundation + Environments (Weeks 1-3)
|
||||
|
||||
## Goal
|
||||
Prepare a runnable technical baseline: on-prem deployment patterns, CI/CD, API contracts, RBAC, service monitoring.
|
||||
|
||||
## Scope
|
||||
- Gateway skeleton
|
||||
- auth-service, users API, audit-service
|
||||
- health endpoints for all services
|
||||
- OpenAPI stage 1 contracts
|
||||
- event schema baseline
|
||||
- docker-compose + Kubernetes + Helm baseline
|
||||
- CI workflow
|
||||
|
||||
## Deliverables
|
||||
- `deployment/*`
|
||||
- `.github/workflows/ci.yml`
|
||||
- `contracts/openapi/stage1-auth-users-health-audit.yaml`
|
||||
- `contracts/events/*.json`
|
||||
|
||||
## Gate 1 DoD
|
||||
- Auto deploy path documented
|
||||
- Login + roles work
|
||||
- Health endpoints available
|
||||
- API/event contracts frozen
|
||||
@@ -1,21 +0,0 @@
|
||||
# Stage 2 - Operator Core (Weeks 4-7)
|
||||
|
||||
## Goal
|
||||
Deliver real operator flow for `voice + Telegram` with interaction lifecycle.
|
||||
|
||||
## Scope
|
||||
- customer-service (cards/search/history)
|
||||
- interaction-service (create/assign/status/escalate/timeline)
|
||||
- routing-service (queues/rules/route)
|
||||
- voice-adapter-service (Asterisk events)
|
||||
- telegram-adapter-service (webhook/messages)
|
||||
|
||||
## Deliverables
|
||||
- operator core APIs in services
|
||||
- stage 2 OpenAPI contracts
|
||||
- smoke scripts and tests for e2e API-level scenarios
|
||||
|
||||
## Gate 2 DoD
|
||||
- Operator can process both channels from unified interaction model
|
||||
- Status and history are consistent
|
||||
- Escalation and assignment work without manual workaround
|
||||
@@ -1,20 +0,0 @@
|
||||
# Stage 3 - Supervisor + Reporting + KB-lite (Weeks 8-10)
|
||||
|
||||
## Goal
|
||||
Add manageability and analytics: realtime supervisor view, KPI core reporting, knowledge base support.
|
||||
|
||||
## Scope
|
||||
- supervisor-service realtime state
|
||||
- reporting-service KPI aggregation: SL, ASA, AHT, Abandon, FCR
|
||||
- kb-service categories/articles/search
|
||||
- stage 3 OpenAPI contracts
|
||||
|
||||
## Deliverables
|
||||
- dashboard-ready supervisor API
|
||||
- reporting export endpoint
|
||||
- KB-lite APIs for operational usage
|
||||
|
||||
## Gate 3 DoD
|
||||
- Supervisor sees realtime operational state
|
||||
- KPI reports reproducible
|
||||
- KB usable in operator flow
|
||||
@@ -1,22 +0,0 @@
|
||||
# Stage 4 - Pilot Hardening + Acceptance (Weeks 11-12)
|
||||
|
||||
## Goal
|
||||
Stabilize and pass pilot acceptance.
|
||||
|
||||
## Scope
|
||||
- load tests up to 100/100
|
||||
- backup/restore drills
|
||||
- security hardening checks
|
||||
- UAT protocol and defect closure (P1/P2)
|
||||
- final documentation pack
|
||||
|
||||
## Deliverables
|
||||
- test protocols
|
||||
- UAT evidence
|
||||
- operational runbooks
|
||||
- accepted MVP v1 baseline
|
||||
|
||||
## Gate 4 DoD
|
||||
- Pilot passed
|
||||
- SLA/KPI acceptance criteria confirmed
|
||||
- System ready for post-MVP expansion
|
||||
@@ -1,126 +0,0 @@
|
||||
# Wave 2 - Post-MVP Backlog
|
||||
|
||||
Wave 2 starts only after MVP pilot sign-off is complete and Gate 4 is formally closed.
|
||||
This document captures the first prioritized expansion backlog so pilot defects and
|
||||
new features do not mix.
|
||||
|
||||
## Priority Order
|
||||
|
||||
### 1. LDAP / SSO
|
||||
|
||||
Goal:
|
||||
- replace local-only auth as the primary enterprise login path
|
||||
|
||||
Expected outputs:
|
||||
- LDAP or SSO integration design
|
||||
- role mapping rules
|
||||
- operational runbook for identity integration
|
||||
|
||||
Acceptance:
|
||||
- enterprise login works without breaking current RBAC behavior
|
||||
- fallback admin access is documented
|
||||
|
||||
### 2. Additional Channels
|
||||
|
||||
Goal:
|
||||
- expand beyond the current MVP channel baseline
|
||||
|
||||
Expected outputs:
|
||||
- `webchat` handling flow
|
||||
- `email` intake flow
|
||||
- external messenger backlog after `webchat` and `email`
|
||||
|
||||
Acceptance:
|
||||
- new channels reuse the unified interaction model
|
||||
- routing, history, and KPI ingestion stay consistent
|
||||
|
||||
### 3. Dedicated Supervisor/Admin UI
|
||||
|
||||
Goal:
|
||||
- move beyond the single operator shell and provide dedicated operational consoles
|
||||
|
||||
Expected outputs:
|
||||
- supervisor UI for realtime monitoring, control, and reporting
|
||||
- admin UI for users, queues, rules, and configuration
|
||||
|
||||
Acceptance:
|
||||
- critical supervisor and admin workflows no longer depend on the operator shell
|
||||
|
||||
### 4. Recording and Audio Operations
|
||||
|
||||
Goal:
|
||||
- turn the current event-level recording placeholders into a usable product block
|
||||
|
||||
Expected outputs:
|
||||
- recording storage model
|
||||
- playback/download flow
|
||||
- access control and retention rules
|
||||
|
||||
Acceptance:
|
||||
- supervisors can find, review, and export recording artifacts safely
|
||||
|
||||
### 5. Advanced IVR and Voice Flow
|
||||
|
||||
Goal:
|
||||
- introduce configurable IVR flows beyond current baseline voice event intake
|
||||
|
||||
Expected outputs:
|
||||
- IVR flow model
|
||||
- editable configuration or builder
|
||||
- routing integration for IVR outcomes
|
||||
|
||||
Acceptance:
|
||||
- IVR outcomes are traceable in interaction lifecycle and routing decisions
|
||||
|
||||
### 6. Full KPI Catalog
|
||||
|
||||
Goal:
|
||||
- expand reporting from MVP core KPIs to the wider target metric model
|
||||
|
||||
Expected outputs:
|
||||
- KPI mapping backlog based on the extended specification
|
||||
- data model updates
|
||||
- report coverage matrix
|
||||
|
||||
Acceptance:
|
||||
- KPI definitions are reproducible and traceable to the specification
|
||||
|
||||
### 7. Scale-Up and Hardening
|
||||
|
||||
Goal:
|
||||
- move from MVP pilot performance to target production scale
|
||||
|
||||
Expected outputs:
|
||||
- capacity plan
|
||||
- higher load targets
|
||||
- scale validation on the target deployment profile
|
||||
|
||||
Acceptance:
|
||||
- agreed performance target is met with documented evidence
|
||||
|
||||
### 8. Event Bus and Integrations
|
||||
|
||||
Goal:
|
||||
- evolve the platform from contract-ready integration to operational event exchange
|
||||
|
||||
Expected outputs:
|
||||
- message bus integration design
|
||||
- service event publishing/consumption plan
|
||||
- external integration sequencing
|
||||
|
||||
Acceptance:
|
||||
- core domain events are delivered through the selected bus without contract drift
|
||||
|
||||
## Delivery Rules
|
||||
|
||||
- Wave 2 must be planned as a separate delivery track from MVP pilot closure.
|
||||
- MVP defects remain in the MVP remediation lane until sign-off is complete.
|
||||
- New Wave 2 scope is approved only after MVP accepted baseline is frozen.
|
||||
|
||||
## Status Update (2026-03-05)
|
||||
|
||||
- Track 9 (Asterisk AMI lab bridge) is accepted in QA evidence packs.
|
||||
- Track 9.2 cutover prep is ready; production execute requires target context/secrets window.
|
||||
- Track 10 is closed:
|
||||
- see `docs/acceptance/track10/track10-acceptance.md`.
|
||||
- Next execution step: Track 11 (live voice control scope).
|
||||
@@ -1,65 +0,0 @@
|
||||
# Wave 2 / Track 10 - Live Voice Reliability and Realtime SLA
|
||||
|
||||
Track 10 starts after Track 9 QA acceptance is collected and frozen.
|
||||
|
||||
## Status Update (2026-03-05)
|
||||
|
||||
- Track 10 is closed using acceptance package:
|
||||
- `docs/acceptance/track10/track10-acceptance.md`
|
||||
- Closure mode: `GO with exception`
|
||||
- direct p95 `started->ended` measured `33s` vs target `30s`
|
||||
- reliability gates for stuck lifecycle/import are green (`active_no_end=0`, `ended_no_recording_upload=0`)
|
||||
- Follow-up latency tightening is moved to the next track.
|
||||
|
||||
## Goal
|
||||
|
||||
Move from "Asterisk integration works" to "voice lifecycle is predictably near-realtime".
|
||||
|
||||
Required outcome:
|
||||
- `call.started` appears in supervisor flow quickly and consistently.
|
||||
- `call.ended` is not delayed by reconciliation paths.
|
||||
- recording upload latency is measured and controlled.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
- latency/SLA instrumentation for voice lifecycle (`started -> ended -> recording`),
|
||||
- stricter reconcile behavior for stale AMI events,
|
||||
- automated latency report as an acceptance artifact,
|
||||
- Track 10 acceptance checklist and evidence structure.
|
||||
|
||||
Out of scope:
|
||||
- ARI call control,
|
||||
- operator softphone controls,
|
||||
- new business voice features.
|
||||
|
||||
## Baseline SLO (Track 10 target)
|
||||
|
||||
- p95 `call.started -> call.ended` <= `30s` for lab calls.
|
||||
- p95 `call.ended -> recording uploaded` <= `45s`.
|
||||
- unresolved `failed bridge events` = `0` for acceptance window.
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. Latency report script:
|
||||
- `scripts/track10_voice_latency_report.py`
|
||||
2. Runbook:
|
||||
- `docs/runbooks/track10-live-voice-reliability.md`
|
||||
3. Acceptance evidence folder:
|
||||
- `docs/acceptance/track10/<timestamp>/`
|
||||
4. Final decision doc:
|
||||
- `docs/acceptance/track10/track10-acceptance.md`
|
||||
|
||||
## Phase 1 (implemented slice)
|
||||
|
||||
Completed:
|
||||
- added Track 10 latency report script with SLO thresholds and JSON artifacts,
|
||||
- added `direct` vs `reconciled` split in latency metrics,
|
||||
- updated bridge path so `recording.ready` can close call lifecycle immediately when `call.ended` is missing.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Track 10 is accepted when:
|
||||
- latency report is generated from real calls,
|
||||
- all mandatory checks pass for the acceptance window,
|
||||
- `GO` is signed in `track10-acceptance.md`.
|
||||
@@ -1,60 +0,0 @@
|
||||
# Wave 2 / Track 11 - Live Operator Voice Control
|
||||
|
||||
Status: `Accepted`
|
||||
|
||||
Track 11 introduced the first real operator call-control loop on top of the accepted Asterisk bridge.
|
||||
|
||||
## Goal
|
||||
|
||||
Move from event-only voice integration to operator-controlled live calls in the existing operator shell.
|
||||
|
||||
Accepted control path:
|
||||
- operator sees a live inbound call in `/operator`
|
||||
- operator answers media in external `MicroSIP/Zoiper`
|
||||
- operator fixes ownership in the browser with `Принять в работу`
|
||||
- operator can `Завершить` or `Передать`
|
||||
|
||||
Voice media stays outside the browser. No WebRTC in this track.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
- bridge live-call routes under `/asterisk/live-calls`
|
||||
- own-call RBAC for `operator`
|
||||
- operator live-call block
|
||||
- action log and replay-safe behavior
|
||||
- Asterisk event `MVPCCOperatorConnected`
|
||||
- QA acceptance with real call and recording playback
|
||||
|
||||
Out of scope:
|
||||
- ARI
|
||||
- attended transfer or conference
|
||||
- embedded browser softphone
|
||||
- business API redesign
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. Migrations:
|
||||
- `migrations/sql/0011_track11_voice_control_sqlite.sql`
|
||||
- `migrations/sql/0011_track11_voice_control_postgres.sql`
|
||||
2. Backend:
|
||||
- `services/asterisk_bridge_service/app.py`
|
||||
3. Operator UI:
|
||||
- `ui/operator/index.html`
|
||||
- `ui/operator/app.js`
|
||||
4. Asterisk templates:
|
||||
- `deployment/asterisk/pjsip.conf`
|
||||
- `deployment/asterisk/extensions.conf`
|
||||
5. Evidence and docs:
|
||||
- `docs/runbooks/track11-live-operator-call-control.md`
|
||||
- `docs/acceptance/track11/track11-acceptance.md`
|
||||
|
||||
## Accepted baseline
|
||||
|
||||
Track 11 is frozen with these guarantees:
|
||||
- live inbound call visibility works in `/operator`
|
||||
- `claim`, `hangup`, and `blind transfer` persist in bridge action logs
|
||||
- `call.connected` and `call.transferred` are written as voice events
|
||||
- recording import and supervisor playback stay green
|
||||
|
||||
For the current operator UX, use the Track 12 runbook. Track 11 remains the control-path baseline.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Wave 2 / Track 12 - Operator Voice UX Hardening
|
||||
|
||||
Status: `Accepted`
|
||||
|
||||
Track 12 hardens the accepted Track 11 voice-control baseline without changing the telephony architecture.
|
||||
|
||||
## Goal
|
||||
|
||||
Keep the current Asterisk plus `MicroSIP/Zoiper` model, but make the operator voice flow predictable and readable.
|
||||
|
||||
Accepted outcome:
|
||||
- `/operator` shows only real bridge-backed calls
|
||||
- active and recently ended calls are separated
|
||||
- browser actions behave consistently
|
||||
- recent calls stay visible briefly instead of disappearing immediately
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
- `GET /asterisk/recent-calls`
|
||||
- additive live-call metadata
|
||||
- operator UI split into `Активные звонки` and `Только что завершённые`
|
||||
- clearer labels and SIP-versus-browser hint text
|
||||
- removal of interaction-based live-call guessing
|
||||
- QA acceptance for active/recent call behavior
|
||||
|
||||
Out of scope:
|
||||
- WebRTC or browser softphone
|
||||
- claim-first telephony redesign
|
||||
- ARI
|
||||
- attended transfer or conference
|
||||
- production cutover
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. Backend:
|
||||
- `services/asterisk_bridge_service/app.py`
|
||||
- `services/shared/models.py`
|
||||
- `gateway/app.py`
|
||||
2. Operator UI:
|
||||
- `ui/operator/index.html`
|
||||
- `ui/operator/app.js`
|
||||
- `ui/operator/styles.css`
|
||||
3. Evidence and docs:
|
||||
- `docs/runbooks/track12-operator-voice-ux.md`
|
||||
- `docs/acceptance/track12/track12-acceptance.template.md`
|
||||
|
||||
## Accepted baseline
|
||||
|
||||
Track 12 is frozen with these guarantees:
|
||||
- `Живые звонки` never falls back to `interaction:int_*` placeholder cards
|
||||
- `reconciled` service traces do not remove an active call early
|
||||
- `hangup` and `blind transfer` move the card into the recent bucket
|
||||
- recent cards stay visible for the short configured window
|
||||
- Track 11 control-path behavior and recording playback remain green
|
||||
|
||||
This is the current operator voice UX baseline.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Wave 2 / Track 14 - Browser Softphone (WebRTC, QA First)
|
||||
|
||||
Status: `Implemented, QA validation pending`
|
||||
|
||||
Track 14 adds the first browser-native operator media path on top of the accepted Track 11 and Track 12 voice baseline.
|
||||
|
||||
## Goal
|
||||
|
||||
Keep the accepted bridge-backed live-call model and move the operator media path into the browser for the first QA slice.
|
||||
|
||||
Implemented direction:
|
||||
- direct Asterisk WSS
|
||||
- inbound MVP only
|
||||
- browser softphone coexists with `MicroSIP/Zoiper`
|
||||
- existing bridge `claim/hangup/recent-calls` remains authoritative
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. Bridge:
|
||||
- additive route: `/asterisk/browser-softphone/config`
|
||||
- additive status fields: `webrtc_enabled`, `webrtc_ws_url`
|
||||
2. Operator shell:
|
||||
- `Browser Softphone` block
|
||||
- register/disconnect state
|
||||
- incoming call banner
|
||||
- `Answer`, `Hang up`, `Mute/Unmute`
|
||||
- microphone and speaker selectors
|
||||
3. Asterisk lab assets:
|
||||
- `deployment/asterisk/http.conf`
|
||||
- `deployment/asterisk/rtp.conf`
|
||||
- updated `deployment/asterisk/pjsip.conf`
|
||||
|
||||
## Boundaries
|
||||
|
||||
Track 14 does not introduce:
|
||||
- production cutover
|
||||
- ARI
|
||||
- browser-only mandatory mode
|
||||
- DB schema changes
|
||||
- WebRTC backend service outside Asterisk
|
||||
|
||||
The accepted voice system of record remains:
|
||||
- bridge live/recent calls
|
||||
- bridge action log
|
||||
- recording import and supervisor playback
|
||||
@@ -1,30 +0,0 @@
|
||||
# Track 15 - Telegram Chat
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
## Scope
|
||||
|
||||
- real Telegram Bot API webhook
|
||||
- one persistent thread per `chat_id`
|
||||
- 2-way text chat
|
||||
- separate `Telegram` page in `/operator`
|
||||
|
||||
## What changed
|
||||
|
||||
- `telegram-adapter-service` now owns Telegram threads and thread messages
|
||||
- new thread/message routes exist under `/integrations/telegram/threads/*`
|
||||
- inbound messages reuse the same thread and linked interaction
|
||||
- a closed Telegram interaction is reactivated on new inbound activity
|
||||
- operator replies go through Telegram `sendMessage`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Telegram voice/video calls
|
||||
- media/file workflow
|
||||
- admin UI for bot setup
|
||||
|
||||
## Acceptance focus
|
||||
|
||||
- first inbound message creates thread + interaction
|
||||
- repeated inbound on the same `chat_id` reuses the same thread
|
||||
- operator can claim and reply from the dedicated `Telegram` page
|
||||
@@ -1,11 +0,0 @@
|
||||
# Sequential Execution Rules
|
||||
|
||||
1. Stages are executed strictly in sequence.
|
||||
2. A stage starts only when previous gate is formally closed.
|
||||
3. Scope changes are deferred to the next stage backlog.
|
||||
4. Weekly checkpoint + demo is mandatory.
|
||||
5. Every stage has:
|
||||
- scope
|
||||
- deliverables
|
||||
- tests
|
||||
- gate checklist
|
||||
@@ -1,132 +0,0 @@
|
||||
# `.env.production` Checklist (Track 9/11 QA Lab)
|
||||
|
||||
Use this checklist before enabling `ASTERISK_BRIDGE_ENABLED=1`.
|
||||
|
||||
## 1) File handling and secret hygiene
|
||||
|
||||
- [ ] Create local `.env.production` from [`.env.production.template`](/e:/Zhan/.env.production.template).
|
||||
- [ ] Keep `.env.production` out of git (it is ignored in [`.gitignore`](/e:/Zhan/.gitignore)).
|
||||
- [ ] Replace all placeholder values:
|
||||
- `<replace-me>`
|
||||
- `<asterisk-linux-vm-host>`
|
||||
- `<platform_queue_id>`
|
||||
- `<platform_ai_queue_id_or_same_queue_id>`
|
||||
- `<postgres-host>`
|
||||
- `<rabbitmq-host>`
|
||||
- [ ] Verify that AMI/SFTP credentials are not copied into docs, commits, or screenshots.
|
||||
|
||||
## 2) Required environment variables
|
||||
|
||||
### Core
|
||||
|
||||
- [ ] `DATABASE_URL` points to PostgreSQL (not SQLite).
|
||||
- [ ] `INTERACTION_SERVICE_URL` is reachable from `asterisk-bridge-service`.
|
||||
- [ ] `VOICE_ADAPTER_SERVICE_URL` is reachable.
|
||||
- [ ] `RECORDING_SERVICE_URL` is reachable.
|
||||
|
||||
### Track 9 bridge
|
||||
|
||||
- [ ] `ASTERISK_BRIDGE_ENABLED=1`
|
||||
- [ ] `ASTERISK_AMI_HOST` / `ASTERISK_AMI_PORT` are correct.
|
||||
- [ ] `ASTERISK_AMI_USERNAME` / `ASTERISK_AMI_SECRET` are correct.
|
||||
- [ ] `ASTERISK_AMI_EVENT_PREFIX=MVPCC` unless your dialplan intentionally changed it.
|
||||
- [ ] `ASTERISK_QUEUE_MAP_JSON` contains valid JSON object.
|
||||
- [ ] Every `queue_code` in dialplan has a mapped platform `queue_id`.
|
||||
- [ ] `ASTERISK_BRIDGE_AUTH_MODE` chosen intentionally:
|
||||
- `legacy_headers` for Track 9 QA baseline
|
||||
- `bearer_first` for transition hardening
|
||||
- `bearer` for strict service-token mode
|
||||
- [ ] If bearer mode is used, `APP_TOKEN_SECRET` is the same across gateway/services/bridge runtime.
|
||||
- [ ] `ASTERISK_BRIDGE_AUTH_USER` / `ASTERISK_BRIDGE_AUTH_ROLE` match expected service identity.
|
||||
- [ ] `VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS` includes `svc:asterisk-bridge`.
|
||||
- [ ] `RECORDING_IMPORT_TRUSTED_SERVICE_SUBJECTS` includes `svc:asterisk-bridge`.
|
||||
- [ ] For strict Track 9.1 mode, set `RECORDING_IMPORT_ALLOW_ADMIN=0`.
|
||||
|
||||
### Track 11 call-control
|
||||
|
||||
- [ ] `ASTERISK_CALLCONTROL_ENABLED=1` (в QA, если проверяете операторское управление звонком).
|
||||
- [ ] `ASTERISK_CALLCONTROL_ACTION_TIMEOUT_SECONDS` задан (рекомендуемо `10`).
|
||||
- [ ] `ASTERISK_OPERATOR_EXTENSION_MAP_JSON` содержит mapping `operator -> extension`.
|
||||
- пример: `{"operator_a":"2001","operator_b":"2002"}`
|
||||
- [ ] `ASTERISK_TRANSFER_TARGET_MAP_JSON` содержит mapping `queue_code -> extension` для blind transfer.
|
||||
- пример: `{"voice_lab":"2001"}`
|
||||
- [ ] `ASTERISK_CALLCONTROL_CLAIM_CONTEXT` и `ASTERISK_CALLCONTROL_TRANSFER_CONTEXT` совпадают с dialplan contexts.
|
||||
|
||||
### SFTP pickup
|
||||
|
||||
- [ ] `ASTERISK_SFTP_HOST` / `ASTERISK_SFTP_PORT` are correct.
|
||||
- [ ] `ASTERISK_SFTP_USERNAME` / `ASTERISK_SFTP_PASSWORD` are correct.
|
||||
- [ ] `ASTERISK_SFTP_BASE_PATH` points to the recording directory (default `/var/spool/asterisk/monitor`).
|
||||
|
||||
### Voice AI V1 staging
|
||||
|
||||
- [ ] `AI_VOICE_ENABLED=1` only for the pilot queues you intend to test.
|
||||
- [ ] `AI_VOICE_QUEUE_CONFIG_JSON` contains the AI-first queue code from the dialplan.
|
||||
- example: `{"voice_lab_ai":{"mode":"ai_first","agent_profile":"voice_support","handoff_queue_code":"voice_lab","language":"ru"}}`
|
||||
- [ ] `AI_VOICE_RUNTIME_SERVICE_URL` points to the HTTP runtime service, not the AudioSocket media endpoint.
|
||||
- [ ] `AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS` includes `svc:ai-voice-runtime`.
|
||||
- [ ] `AI_VOICE_ASR_PROVIDER` / `AI_VOICE_TTS_PROVIDER` are set intentionally.
|
||||
- [ ] `ASTERISK_TRANSFER_TARGET_MAP_JSON` includes both the human and AI queue codes when they can hand off to the same operator group.
|
||||
- example: `{"voice_lab":"2001","voice_lab_ai":"2001"}`
|
||||
- [ ] On the Asterisk VM, leave `MVPCC_AI_AUDIOSOCKET_SERVICE=` blank until the media listener is ready.
|
||||
- [ ] After the media listener is available, verify `app_audiosocket.so` is loaded and `MVPCC_AI_AUDIOSOCKET_SERVICE=<media-host>:<port>` uses the raw AudioSocket address.
|
||||
|
||||
### Event bus baseline (Track 8 compatibility)
|
||||
|
||||
- [ ] `EVENT_BUS_ENABLED=1` in QA.
|
||||
- [ ] `EVENT_BUS_URL` resolves to reachable RabbitMQ.
|
||||
|
||||
## 3) Preflight checks before first call
|
||||
|
||||
- [ ] Run migrations:
|
||||
- `python scripts\migrate_core_db.py`
|
||||
- [ ] Run full regression:
|
||||
- `pytest -q`
|
||||
- [ ] Start stack with production env loaded:
|
||||
- `powershell -ExecutionPolicy Bypass -File scripts\start_track9_qa.ps1`
|
||||
- [ ] Bridge health:
|
||||
- `GET /proxy/asterisk-bridge/health` returns `ok`
|
||||
- [ ] Bridge status:
|
||||
- `GET /proxy/asterisk-bridge/asterisk/status` shows `ami_connected=true`
|
||||
|
||||
## 4) Acceptance checks after test call `1001 -> 7000`
|
||||
|
||||
- [ ] `voice` interaction created from Asterisk event flow.
|
||||
- [ ] `voice_events` contain:
|
||||
- `call.started` with `payload.source = "asterisk"`
|
||||
- `call.ended` for the same `call_id`
|
||||
- [ ] `MVPCCRecordingReady` led to uploaded recording in `recording-service`.
|
||||
- [ ] Playback works in `supervisor`.
|
||||
- [ ] Для Track 11:
|
||||
- оператор видит звонок в `/operator` -> блок `Живые звонки`
|
||||
- `claim` работает и пишет action log
|
||||
- `hangup` работает
|
||||
- `blind transfer` работает
|
||||
- [ ] Run:
|
||||
- `python scripts\asterisk_lab_smoke.py --base-url <gateway> --database-url <db> --require-recording`
|
||||
- `python scripts\track9_check.py --base-url <gateway> --database-url <db> --require-recording`
|
||||
- `python scripts\track9_collect_evidence.py --base-url <gateway> --database-url <db> --run-checks --require-recording`
|
||||
|
||||
## 4.1) Voice AI control-plane smoke after test call `1001 -> 7100`
|
||||
|
||||
- [ ] `MVPCCCallStarted` used `QueueCode=voice_lab_ai`.
|
||||
- [ ] `voice_ai_sessions` row created for the call.
|
||||
- [ ] `ai_sessions` row created with `channel=voice`.
|
||||
- [ ] Interaction timeline contains `ai.session_started`.
|
||||
- [ ] Operator UI shows an AI badge on the live or recent voice call card.
|
||||
- [ ] `GET /proxy/asterisk-bridge/asterisk/live-calls/{call_id}/ai-summary` returns `200` once the AI summary exists.
|
||||
- [ ] Human fallback path still works if `MVPCC_AI_AUDIOSOCKET_SERVICE=` is blank.
|
||||
|
||||
## 5) Failure policy
|
||||
|
||||
- [ ] If failed bridge events exist, inspect:
|
||||
- `GET /proxy/asterisk-bridge/asterisk/events?status=failed`
|
||||
- [ ] Retry after fixing root cause:
|
||||
- `POST /proxy/asterisk-bridge/asterisk/events/{bridge_event_id}/retry`
|
||||
- [ ] Do not mark Track 9 accepted until failed bridge events are resolved or explicitly waived.
|
||||
|
||||
## 6) Track 9.2 cutover handoff
|
||||
|
||||
- [ ] Prepare [track9-cutover-sheet.template.md](/e:/Zhan/docs/acceptance/track9/track9-cutover-sheet.template.md).
|
||||
- [ ] Follow [track9-2-production-cutover.md](/e:/Zhan/docs/runbooks/track9-2-production-cutover.md) for controlled Helm rollout.
|
||||
- [ ] Use `scripts\track9_2_cutover.ps1` first in dry-run mode, then with `-Execute` during the approved window.
|
||||
@@ -1,315 +0,0 @@
|
||||
# Asterisk Lab on Linux VM
|
||||
|
||||
Use this runbook to stand up the Track 9 human baseline, the IVR staging entrypoint, and the Voice AI V1 staging entrypoint on the same Asterisk VM.
|
||||
|
||||
## Target
|
||||
|
||||
- Host: dedicated Linux VM
|
||||
- OS: Ubuntu 22.04 LTS or Ubuntu 24.04 LTS
|
||||
- Asterisk: 20 LTS
|
||||
- Integration mode: AMI + custom `UserEvent`
|
||||
- Call source: softphone lab only
|
||||
- Human baseline entrypoint: `7000`
|
||||
- IVR staging entrypoint: `7200`
|
||||
- Voice AI staging entrypoint: `7100`
|
||||
|
||||
## Prepare the VM
|
||||
|
||||
1. Install Asterisk 20 LTS.
|
||||
2. Ensure these ports are reachable from the platform host:
|
||||
- `5060/udp` for SIP
|
||||
- `5038/tcp` for AMI
|
||||
- `22/tcp` for SFTP
|
||||
3. Create the recording directory:
|
||||
- `/var/spool/asterisk/monitor/mvpcc`
|
||||
|
||||
## Apply repo configs
|
||||
|
||||
Copy these files from the repository:
|
||||
|
||||
- `deployment/asterisk/pjsip.conf`
|
||||
- `deployment/asterisk/extensions.conf`
|
||||
- `deployment/asterisk/manager.conf`
|
||||
|
||||
Before reload:
|
||||
|
||||
1. Replace default passwords.
|
||||
2. Confirm extension `1001`.
|
||||
3. Keep extension `7000` as the human-only inbound test entrypoint.
|
||||
4. Keep extension `7100` reserved for Voice AI staging.
|
||||
5. Keep extension `7200` reserved for IVR staging.
|
||||
|
||||
Voice AI dialplan notes:
|
||||
|
||||
- `deployment/asterisk/extensions.conf` now defines two queue codes:
|
||||
- `voice_lab` for `7000`
|
||||
- `voice_lab_ai` for `7100`
|
||||
- `7100` falls back to the human dial targets unless `MVPCC_AI_AUDIOSOCKET_SERVICE` is populated.
|
||||
- When you are ready to test the media bridge, load `app_audiosocket.so` and set `MVPCC_AI_AUDIOSOCKET_SERVICE=<media-host>:<port>`.
|
||||
- Asterisk `AudioSocket()` requires a raw TCP media listener and a UUID per call. The template generates the UUID with `uuidgen`.
|
||||
- Official app reference:
|
||||
- [Asterisk AudioSocket application](https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/AudioSocket/)
|
||||
|
||||
IVR dialplan notes:
|
||||
|
||||
- `deployment/asterisk/extensions.conf` defines `voice_lab_ivr` for `7200`.
|
||||
- `7200` calls FastAGI at `MVPCC_IVR_FASTAGI_HOSTPORT` and expects `prompt_audio_key` values to match pre-provisioned Asterisk sound files.
|
||||
- If FastAGI does not return `MVPCC_IVR_TARGET_EXTENSION`, the call falls back to the human dial targets.
|
||||
|
||||
## Configure the platform bridge
|
||||
|
||||
Set these environment variables for `asterisk-bridge-service`:
|
||||
|
||||
- `ASTERISK_BRIDGE_ENABLED=1`
|
||||
- `ASTERISK_AMI_HOST=<linux-vm-host>`
|
||||
- `ASTERISK_AMI_PORT=5038`
|
||||
- `ASTERISK_AMI_USERNAME=mvpcc`
|
||||
- `ASTERISK_AMI_SECRET=<ami-secret>`
|
||||
- `ASTERISK_AMI_EVENT_PREFIX=MVPCC`
|
||||
- `ASTERISK_QUEUE_MAP_JSON={"voice_lab":"<platform_queue_id>","voice_lab_ai":"<platform_ai_queue_id_or_same_queue_id>","voice_lab_ivr":"<platform_ivr_queue_id>"}`
|
||||
- `ASTERISK_BRIDGE_AUTH_MODE=legacy_headers` (or `bearer_first` / `bearer`)
|
||||
- `ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY=1` (used only with `bearer_first`)
|
||||
- `ASTERISK_BRIDGE_AUTH_SUBJECT=svc:asterisk-bridge`
|
||||
- `ASTERISK_BRIDGE_AUTH_USER=asterisk-bridge`
|
||||
- `ASTERISK_BRIDGE_AUTH_ROLE=admin`
|
||||
- `ASTERISK_BRIDGE_AUTH_TOKEN_TTL_SECONDS=300`
|
||||
- `ASTERISK_IVR_FASTAGI_ENABLED=1`
|
||||
- `ASTERISK_IVR_FASTAGI_HOST=0.0.0.0` (or the explicit bridge bind IP)
|
||||
- `ASTERISK_IVR_FASTAGI_PORT=4573`
|
||||
- `ASTERISK_IVR_DTMF_TIMEOUT_SECONDS=5`
|
||||
- `ASTERISK_IVR_MAX_NO_INPUT_RETRIES=2`
|
||||
- `ASTERISK_IVR_MAX_INVALID_RETRIES=2`
|
||||
- `IVR_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:asterisk-bridge`
|
||||
- `VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS=svc:asterisk-bridge,svc:ivr-service`
|
||||
- `RECORDING_IMPORT_TRUSTED_SERVICE_SUBJECTS=svc:asterisk-bridge`
|
||||
- `RECORDING_IMPORT_ALLOW_ADMIN=1` (set `0` in strict mode)
|
||||
- `ASTERISK_SFTP_HOST=<linux-vm-host>`
|
||||
- `ASTERISK_SFTP_PORT=22`
|
||||
- `ASTERISK_SFTP_USERNAME=<linux-user>`
|
||||
- `ASTERISK_SFTP_PASSWORD=<linux-password>`
|
||||
- `ASTERISK_SFTP_BASE_PATH=/var/spool/asterisk/monitor`
|
||||
- `ASTERISK_TRANSFER_TARGET_MAP_JSON={"voice_lab":"2001","voice_lab_ai":"2001","voice_lab_ivr":"7200"}`
|
||||
- `AI_VOICE_ENABLED=1`
|
||||
- `AI_VOICE_QUEUE_CONFIG_JSON={"voice_lab_ai":{"mode":"ai_first","agent_profile":"voice_support","handoff_queue_code":"voice_lab","language":"ru"}}`
|
||||
- `AI_VOICE_RUNTIME_SERVICE_URL=http://ai-voice-runtime-service:8000`
|
||||
- `AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:ai-voice-runtime`
|
||||
- `AI_VOICE_ASR_PROVIDER=openai`
|
||||
- `AI_VOICE_TTS_PROVIDER=openai`
|
||||
- `AI_VOICE_MAX_CONTEXT_SEGMENTS=8`
|
||||
- `AI_VOICE_HANDOFF_TIMEOUT_SECONDS=8`
|
||||
|
||||
For Track 9 QA acceptance baseline, keep:
|
||||
|
||||
- bridge auth mode in compatibility headers (`X-User` / `X-Role`)
|
||||
- this is expected for Track 9 baseline and moved to hardening in Track 9.1
|
||||
|
||||
For Track 9.1 hardening, switch to:
|
||||
|
||||
- `ASTERISK_BRIDGE_AUTH_MODE=bearer_first` first
|
||||
- then `ASTERISK_BRIDGE_AUTH_MODE=bearer` after successful smoke and no auth regressions
|
||||
- set `RECORDING_IMPORT_ALLOW_ADMIN=0`
|
||||
- in Helm-based environments, use `deployment/helm/values.track9-strict.yaml` as an overlay
|
||||
|
||||
Voice AI V1 staging guidance:
|
||||
|
||||
- Start with `MVPCC_AI_AUDIOSOCKET_SERVICE=` blank in `extensions.conf`.
|
||||
- This lets `7100` validate the AI control-plane wiring while still falling back to the existing human voice path.
|
||||
- Only point `MVPCC_AI_AUDIOSOCKET_SERVICE` to a live AudioSocket listener after the media listener is reachable from the VM.
|
||||
- `AI_VOICE_RUNTIME_SERVICE_URL` is not the AudioSocket address; it is only the HTTP control-plane URL.
|
||||
|
||||
## Register the softphone
|
||||
|
||||
In your softphone:
|
||||
|
||||
- username: `1001`
|
||||
- password: the value set in `pjsip.conf`
|
||||
- server: the Linux VM IP/hostname
|
||||
|
||||
## QA preflight (required)
|
||||
|
||||
1. Copy [`.env.production.template`](/e:/Zhan/.env.production.template) to local `.env.production`.
|
||||
2. Complete [`.env.production.checklist.md`](/e:/Zhan/docs/runbooks/.env.production.checklist.md).
|
||||
3. Start the stack with production env loaded:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\start_track9_qa.ps1
|
||||
```
|
||||
|
||||
4. Run:
|
||||
|
||||
```powershell
|
||||
python scripts\migrate_core_db.py
|
||||
pytest -q
|
||||
python scripts\track9_preflight.py --base-url http://127.0.0.1:8080 --check-sftp
|
||||
python scripts\track9_preflight.py --base-url http://127.0.0.1:8080 --check-sftp --require-strict-service-auth
|
||||
```
|
||||
|
||||
## Run the first test call
|
||||
|
||||
1. Start the platform.
|
||||
2. Start `asterisk-bridge-service`.
|
||||
3. Call `7000` from extension `1001`.
|
||||
4. Hang up after the playback finishes.
|
||||
|
||||
Expected result:
|
||||
|
||||
- `asterisk-bridge-service` receives:
|
||||
- `MVPCCCallStarted`
|
||||
- `MVPCCCallEnded`
|
||||
- `MVPCCRecordingReady`
|
||||
- platform creates a new `voice` interaction
|
||||
- `voice_events` contain:
|
||||
- `call.started`
|
||||
- `call.ended`
|
||||
- one recording is uploaded into `recording-service`
|
||||
|
||||
## Run the IVR-over-Asterisk smoke
|
||||
|
||||
1. Create or activate an IVR flow for the queue mapped from `voice_lab_ivr`.
|
||||
2. Make sure every live node uses a valid `prompt_audio_key`, and the audio files are present in Asterisk sounds.
|
||||
3. Call `7200` from extension `1001`.
|
||||
4. Test both branches:
|
||||
- enter `1` or `2` to complete IVR and transfer into the mapped queue target
|
||||
- stay silent until retry exhaustion to verify fallback into the human baseline
|
||||
|
||||
Expected result:
|
||||
|
||||
- `asterisk-bridge-service` receives `MVPCCCallStarted` with `QueueCode=voice_lab_ivr`.
|
||||
- `ivr_sessions` contains a live session for the `call_id`.
|
||||
- interaction timeline contains `ivr.session.started` and `ivr.step.no_input` or `ivr.step.completed`.
|
||||
- terminal completion writes `voice_events.event_type="ivr.completed"`.
|
||||
- telephony returns to `mvpcc-transfer` for the resolved queue target, or to the human baseline on retry exhaustion.
|
||||
|
||||
## Run the Voice AI control-plane smoke
|
||||
|
||||
Use this smoke before turning on the real AudioSocket media path.
|
||||
|
||||
1. Keep `MVPCC_AI_AUDIOSOCKET_SERVICE=` blank in `extensions.conf`.
|
||||
2. Start the platform and `ai-voice-runtime-service`.
|
||||
3. Call `7100` from extension `1001`.
|
||||
4. Let the call ring through to the fallback human dial targets.
|
||||
|
||||
Expected result:
|
||||
|
||||
- `asterisk-bridge-service` receives `MVPCCCallStarted` with `QueueCode=voice_lab_ai`.
|
||||
- platform creates:
|
||||
- a `voice` interaction
|
||||
- a `voice_ai_sessions` row
|
||||
- an `ai_sessions` row with `channel=voice`
|
||||
- operator UI shows the AI badge on the live/recent call card
|
||||
- normal call recording flow stays green
|
||||
|
||||
This smoke proves queue selection, bridge-to-runtime session creation, timeline events, and safe human fallback. It does not prove realtime ASR/TTS yet.
|
||||
|
||||
## Verify from the platform
|
||||
|
||||
1. Check:
|
||||
- `GET /proxy/asterisk-bridge/asterisk/status`
|
||||
2. Then run:
|
||||
|
||||
```powershell
|
||||
python scripts\track9_preflight.py --base-url http://127.0.0.1:8080 --check-sftp
|
||||
```
|
||||
|
||||
3. For smoke validation:
|
||||
|
||||
```powershell
|
||||
python scripts\asterisk_lab_smoke.py --base-url http://127.0.0.1:8080 --database-url <database-url> --require-recording
|
||||
```
|
||||
|
||||
4. For formal validation:
|
||||
|
||||
```powershell
|
||||
python scripts\track9_check.py --base-url http://127.0.0.1:8080 --database-url <database-url> --require-recording
|
||||
```
|
||||
|
||||
5. For Voice AI control-plane validation, also verify:
|
||||
- `GET /proxy/asterisk-bridge/asterisk/live-calls`
|
||||
- `GET /proxy/asterisk-bridge/asterisk/live-calls/{call_id}/ai-summary`
|
||||
- `ai.session_started` exists in the interaction timeline for the `7100` call
|
||||
|
||||
## Build formal acceptance evidence pack
|
||||
|
||||
After smoke/check pass:
|
||||
|
||||
```powershell
|
||||
python scripts\track9_collect_evidence.py --base-url http://127.0.0.1:8080 --database-url <database-url> --run-checks --require-recording
|
||||
```
|
||||
|
||||
Then fill:
|
||||
|
||||
- `docs/acceptance/track9/<timestamp>/track9-acceptance.md`
|
||||
|
||||
At minimum include:
|
||||
|
||||
- bridge status snapshot
|
||||
- smoke output
|
||||
- track9_check output
|
||||
- sample rows from:
|
||||
- `asterisk_event_log`
|
||||
- `voice_events`
|
||||
- recording linkage
|
||||
|
||||
## Retry semantics and duplicate safety
|
||||
|
||||
Bridge event statuses:
|
||||
|
||||
- `received`: event accepted by bridge, no forward result yet.
|
||||
- `forwarded`: event processed successfully and forwarded to platform services.
|
||||
- `failed`: forwarding failed; inspect `last_error`, fix root cause, then retry.
|
||||
|
||||
Retry endpoint:
|
||||
|
||||
- `POST /proxy/asterisk-bridge/asterisk/events/{bridge_event_id}/retry`
|
||||
|
||||
Expected behavior:
|
||||
|
||||
1. Retry is safe for the same `bridge_event_id`.
|
||||
2. Existing `call_id -> interaction_id` links prevent duplicate interaction creation.
|
||||
3. Recording retry will update missing upload state if SFTP/upload issue is fixed.
|
||||
4. Keep retries manual in QA until root cause is verified.
|
||||
|
||||
## Timeout / retry policy baseline (QA)
|
||||
|
||||
| Area | Current baseline | Operator action |
|
||||
|---|---|---|
|
||||
| AMI reconnect | `ASTERISK_BRIDGE_POLL_INTERVAL_SECONDS` (default `1s`) reconnect loop | Verify `ami_connected=true` in `/asterisk/status` |
|
||||
| SFTP fetch | Single attempt per event processing | Fix credentials/path/network, then call retry endpoint |
|
||||
| Recording upload | Single upload attempt per event processing | Fix `recording-service` reachability/auth, then retry |
|
||||
| Failed event aging | No auto-waiver | Track 9 is not accepted with unresolved failed events |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- `AMI is not connected`
|
||||
- verify `manager.conf`, firewall, and credentials
|
||||
- `Unknown QueueCode`
|
||||
- ensure dialplan `QueueCode` matches `ASTERISK_QUEUE_MAP_JSON`
|
||||
- for Voice AI staging, verify both `voice_lab` and `voice_lab_ai`
|
||||
- verify mapped `queue_id` exists in platform:
|
||||
- `GET /proxy/routing/queues`
|
||||
- re-run failed event with retry endpoint
|
||||
- `7100` immediately falls back to a human and no AI badge appears
|
||||
- verify `AI_VOICE_ENABLED=1`
|
||||
- verify `AI_VOICE_QUEUE_CONFIG_JSON` contains `voice_lab_ai`
|
||||
- verify the `MVPCCCallStarted` event shows `QueueCode=voice_lab_ai`
|
||||
- `7100` errors as soon as AudioSocket is enabled
|
||||
- verify `app_audiosocket.so` is loaded on Asterisk
|
||||
- verify `MVPCC_AI_AUDIOSOCKET_SERVICE` points to a raw TCP media listener, not `http://...:8000`
|
||||
- verify the media host and port are reachable from the Asterisk VM
|
||||
- `No uploaded recordings linked to Asterisk events`
|
||||
- verify SFTP credentials and `/var/spool/asterisk/monitor/mvpcc`
|
||||
- verify `ASTERISK_SFTP_BASE_PATH`
|
||||
- verify `POST /proxy/recording/recordings/import-upload` path is reachable from bridge host
|
||||
- retry failed `MVPCCRecordingReady` events after fix
|
||||
- `Failed bridge events present`
|
||||
- inspect:
|
||||
- `GET /proxy/asterisk-bridge/asterisk/events?status=failed`
|
||||
- then retry with:
|
||||
- `POST /proxy/asterisk-bridge/asterisk/events/{bridge_event_id}/retry`
|
||||
|
||||
## Track 9.2 production cutover
|
||||
|
||||
After QA acceptance is signed, switch to controlled production rollout:
|
||||
|
||||
- [track9-2-production-cutover.md](/e:/Zhan/docs/runbooks/track9-2-production-cutover.md)
|
||||
- `scripts/track9_2_cutover.ps1`
|
||||
@@ -1,20 +0,0 @@
|
||||
# Runbook - Backup and Restore (Stage 4)
|
||||
|
||||
## Backup
|
||||
- Stop write-heavy operations (or enter maintenance window).
|
||||
- Run backup script:
|
||||
- `powershell -ExecutionPolicy Bypass -File scripts/backup_data.ps1`
|
||||
- Keep backup config (`values.yaml`, contracts, runbooks).
|
||||
|
||||
## Restore drill
|
||||
1. Start clean environment
|
||||
2. Re-apply deployment manifests
|
||||
3. Restore snapshots:
|
||||
- `powershell -ExecutionPolicy Bypass -File scripts/restore_data.ps1 -BackupZip <zip-path>`
|
||||
4. Validate smoke scenarios:
|
||||
- login
|
||||
- create interaction
|
||||
- KPI endpoint
|
||||
|
||||
## Acceptance
|
||||
- RTO and data integrity documented in pilot protocol
|
||||
@@ -1,79 +0,0 @@
|
||||
# Demo Showcase Runbook
|
||||
|
||||
Use this flow before showing the MVP to management.
|
||||
|
||||
## One-command prep
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\prepare_demo.ps1
|
||||
```
|
||||
|
||||
What it does:
|
||||
|
||||
- starts the full local stack in the background
|
||||
- clears local demo data before start
|
||||
- runs the smoke check against `http://localhost:8080`
|
||||
- seeds demo data for customers, interactions, integrations, KB, supervisor, KPI, and one sample recording
|
||||
|
||||
## What to open
|
||||
|
||||
- Operator UI: `http://localhost:8080/operator`
|
||||
- Supervisor UI: `http://localhost:8080/supervisor`
|
||||
- Admin UI: `http://localhost:8080/admin`
|
||||
- Logs: `.local_stack\logs`
|
||||
- Demo summary: `.local_stack\demo-seed-summary.json`
|
||||
- Login: `admin / admin123`
|
||||
|
||||
## Demo sequence
|
||||
|
||||
1. Log in as `admin / admin123`.
|
||||
2. Open the interactions list and show:
|
||||
- one voice interaction already assigned, escalated, and closed
|
||||
- one active follow-up interaction
|
||||
3. Show that assign / escalate uses default values (`operator_a`, `line2`) without browser prompts.
|
||||
4. Trigger the voice channel, Telegram, Webchat, and Email checks.
|
||||
5. Search KB for `demo-showcase`.
|
||||
6. Refresh the supervisor block and read the summary cards.
|
||||
7. Refresh KPI and read the summary cards.
|
||||
- the JSON payload also includes extended metrics and a `by_channel` breakdown
|
||||
8. Open `/supervisor`, load `Recordings`, and show playback/download of the seeded sample.
|
||||
9. Open `/admin`, load the seeded IVR flow, show the completed IVR session, and preview route override with `ivr_session_id`.
|
||||
|
||||
## Optional Telegram chat follow-up
|
||||
|
||||
If the Telegram bot env is configured:
|
||||
|
||||
1. Open `/operator`.
|
||||
2. Go to the `Telegram` page in the sidebar.
|
||||
3. Send one test message to the configured bot from a real Telegram client.
|
||||
4. Show that:
|
||||
- a persistent thread appears for that `chat_id`
|
||||
- the linked interaction uses `channel="telegram"`
|
||||
- the operator can claim the thread and reply from the same page
|
||||
|
||||
## Optional live voice follow-up
|
||||
|
||||
If the Asterisk lab bridge is enabled on a dedicated Linux VM:
|
||||
|
||||
1. Open `/operator`.
|
||||
2. Choose the QA media path:
|
||||
- browser softphone via `Browser Softphone`
|
||||
- or external fallback via `MicroSIP/Zoiper`
|
||||
3. Place one lab call from softphone `1001` to `7000`.
|
||||
4. If using browser media, connect the browser softphone first and answer in Chrome/Edge.
|
||||
5. Use the current voice baseline from `docs/runbooks/voice-baseline.md`.
|
||||
|
||||
## Optional separate shells
|
||||
|
||||
After the main management flow, you can open:
|
||||
|
||||
- `/supervisor` to show dedicated realtime and queue controls
|
||||
- `/admin` to show separate user and queue management
|
||||
|
||||
## Stop after the demo
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\stop_all_local.ps1
|
||||
```
|
||||
|
||||
For a short speaking script during the meeting, use `docs/runbooks/management-demo-brief.md`.
|
||||
@@ -1,175 +0,0 @@
|
||||
# Runbook - On-Prem Deployment
|
||||
|
||||
## Helm
|
||||
- `helm upgrade --install mvp-cc ./deployment/helm -n mvp-cc --create-namespace`
|
||||
- For PostgreSQL-backed Helm releases, the chart now runs `scripts/migrate_core_db.py` automatically via a pre-install / pre-upgrade migration Job.
|
||||
|
||||
## Raw manifests
|
||||
- `kubectl apply -f deployment/kubernetes/mvp-cc-platform.yaml`
|
||||
- Raw manifests remain a minimal reference/bootstrap path.
|
||||
- Helm is the canonical production-scale deployment path for Wave 2 / Track 7.
|
||||
|
||||
## Core DB migration
|
||||
- Helm path:
|
||||
- keep `migrations.enabled=true`
|
||||
- keep `SCHEMA_MANAGEMENT_MODE=migrations`
|
||||
- the Helm hook Job applies migrations before rollout
|
||||
- Raw manifests / manual path:
|
||||
- run `python scripts/migrate_core_db.py`
|
||||
- then start services with `SCHEMA_MANAGEMENT_MODE=migrations`
|
||||
|
||||
## Wave 2 / Track 7 scale baseline
|
||||
|
||||
Production-like K8s scale validation requires:
|
||||
|
||||
- `DATABASE_URL` set to a shared PostgreSQL endpoint
|
||||
- `DB_POOL_SIZE`
|
||||
- `DB_MAX_OVERFLOW`
|
||||
- `DB_POOL_TIMEOUT_SECONDS`
|
||||
- `DB_POOL_RECYCLE_SECONDS`
|
||||
- a working `metrics-server` in the target cluster
|
||||
|
||||
Recommended Wave 2 / Track 7 rollout:
|
||||
|
||||
1. Render Helm config for the scale profile.
|
||||
2. Confirm `DATABASE_URL` points to PostgreSQL (not SQLite).
|
||||
3. Confirm `SCHEMA_MANAGEMENT_MODE=migrations`.
|
||||
4. Confirm `migrations.enabled=true`.
|
||||
5. Deploy via Helm.
|
||||
6. Verify the migration Job completed successfully.
|
||||
7. Run `python scripts/load_test.py --base-url http://<gateway-host> --profile step_250_250 --auth-mode bearer`.
|
||||
8. Run `python scripts/track7_check.py --namespace mvp-cc --report-dir <report_dir>`.
|
||||
9. Repeat with `--profile target_500_500`.
|
||||
|
||||
For the complete procedure, use:
|
||||
|
||||
- [track7-scale-validation.md](/e:/Zhan/docs/runbooks/track7-scale-validation.md)
|
||||
|
||||
## Wave 2 / Track 1 auth configuration
|
||||
|
||||
Minimum enterprise auth variables:
|
||||
|
||||
- `APP_TOKEN_SECRET`
|
||||
- `APP_TOKEN_TTL_SECONDS` (default `3600`)
|
||||
- `ALLOW_LEGACY_HEADER_AUTH=0`
|
||||
- `OIDC_ENABLED=1`
|
||||
- `OIDC_PROVIDER=keycloak`
|
||||
- `OIDC_ISSUER_URL`
|
||||
- `OIDC_CLIENT_ID`
|
||||
- `OIDC_CLIENT_SECRET`
|
||||
- `OIDC_REDIRECT_URI`
|
||||
- `OIDC_SCOPES` (default `openid profile email`)
|
||||
- `OIDC_ROLE_CLAIM` (default `groups`)
|
||||
- `OIDC_ROLE_MAP_JSON`
|
||||
|
||||
Helm and raw Kubernetes manifests now include placeholder values for these settings.
|
||||
Replace all `change-me-*` values before deploying outside local/demo.
|
||||
|
||||
Recommended rollout sequence:
|
||||
|
||||
1. For Helm, keep migration hook enabled so schema upgrades run before deploy.
|
||||
2. Deploy with `OIDC_ENABLED=1` and `ALLOW_LEGACY_HEADER_AUTH=1` in QA.
|
||||
3. Run `python scripts/oidc_smoke.py --base-url http://<gateway-host> --require-enabled`.
|
||||
4. Validate a real Keycloak login round trip.
|
||||
5. Switch enterprise pilot to `ALLOW_LEGACY_HEADER_AUTH=0`.
|
||||
|
||||
## Post-deploy checks
|
||||
- Pods ready state
|
||||
- `/health` probes green
|
||||
- Gateway registry endpoint available
|
||||
- `GET /proxy/auth/auth/oidc/config` returns `enabled=true`
|
||||
- `GET /proxy/auth/auth/oidc/health` returns provider metadata status
|
||||
|
||||
See also:
|
||||
|
||||
- [keycloak-oidc.md](/e:/Zhan/docs/runbooks/keycloak-oidc.md)
|
||||
- [postgres-dev-cutover.md](/e:/Zhan/docs/runbooks/postgres-dev-cutover.md)
|
||||
- [postgres-server-docker.md](/e:/Zhan/docs/runbooks/postgres-server-docker.md)
|
||||
- [postgres-server-parallel-stack.md](/e:/Zhan/docs/runbooks/postgres-server-parallel-stack.md)
|
||||
|
||||
## Wave 2 / Track 9.1 strict bridge auth cutover
|
||||
|
||||
Use Helm overlays to force strict bridge auth mode in QA/enterprise environments:
|
||||
|
||||
```powershell
|
||||
helm upgrade --install mvp-cc deployment\helm `
|
||||
-f deployment\helm\values.scale500.yaml `
|
||||
-f deployment\helm\values.track9-strict.yaml `
|
||||
--set-string auth.appTokenSecret=<shared-app-token-secret> `
|
||||
--set-string asteriskBridge.enabled=1 `
|
||||
--set-string asteriskBridge.amiHost=<asterisk-vm-host> `
|
||||
--set-string asteriskBridge.amiUsername=<ami-user> `
|
||||
--set-string asteriskBridge.amiSecret=<ami-secret> `
|
||||
--set-string asteriskBridge.queueMapJson='{\"voice_lab\":\"<queue_id>\"}' `
|
||||
--set-string asteriskBridge.sftpHost=<asterisk-vm-host> `
|
||||
--set-string asteriskBridge.sftpUsername=<sftp-user> `
|
||||
--set-string asteriskBridge.sftpPassword=<sftp-password> `
|
||||
-n mvp-cc --create-namespace
|
||||
```
|
||||
|
||||
Expected strict baseline after deploy:
|
||||
|
||||
- `ALLOW_LEGACY_HEADER_AUTH=0`
|
||||
- `ASTERISK_BRIDGE_AUTH_MODE=bearer`
|
||||
- `ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY=0`
|
||||
- `VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS` contains `svc:asterisk-bridge`
|
||||
- `RECORDING_IMPORT_TRUSTED_SERVICE_SUBJECTS` contains `svc:asterisk-bridge`
|
||||
- `RECORDING_IMPORT_ALLOW_ADMIN=0`
|
||||
|
||||
Post-cutover validation:
|
||||
|
||||
```powershell
|
||||
python scripts\track9_preflight.py --base-url http://<gateway-host> --check-sftp
|
||||
python scripts\asterisk_lab_smoke.py --base-url http://<gateway-host> --database-url <database-url> --require-recording
|
||||
python scripts\track9_check.py --base-url http://<gateway-host> --database-url <database-url> --require-recording
|
||||
```
|
||||
|
||||
## Wave 2 / Track 9.2 controlled production cutover
|
||||
|
||||
Use the dedicated cutover runbook and script:
|
||||
|
||||
- [track9-2-production-cutover.md](/e:/Zhan/docs/runbooks/track9-2-production-cutover.md)
|
||||
- `scripts/track9_2_cutover.ps1`
|
||||
|
||||
Dry-run (render/lint/snapshot only):
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\track9_2_cutover.ps1 `
|
||||
-KubeContext <context> `
|
||||
-Namespace <namespace> `
|
||||
-Release <release> `
|
||||
-GatewayBaseUrl http://<gateway-host> `
|
||||
-DatabaseUrl postgresql://<...> `
|
||||
-ImageTag <image-tag> `
|
||||
-AmiHost <asterisk-vm-host> `
|
||||
-AmiUser <ami-user> `
|
||||
-AmiSecret <ami-secret> `
|
||||
-SftpHost <asterisk-vm-host> `
|
||||
-SftpUser <sftp-user> `
|
||||
-SftpPassword <sftp-password> `
|
||||
-QueueId <voice-lab-queue-id> `
|
||||
-AppTokenSecret <shared-app-token-secret>
|
||||
```
|
||||
|
||||
Execute cutover:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\track9_2_cutover.ps1 `
|
||||
-KubeContext <context> `
|
||||
-Namespace <namespace> `
|
||||
-Release <release> `
|
||||
-GatewayBaseUrl http://<gateway-host> `
|
||||
-DatabaseUrl postgresql://<...> `
|
||||
-ImageTag <image-tag> `
|
||||
-AmiHost <asterisk-vm-host> `
|
||||
-AmiUser <ami-user> `
|
||||
-AmiSecret <ami-secret> `
|
||||
-SftpHost <asterisk-vm-host> `
|
||||
-SftpUser <sftp-user> `
|
||||
-SftpPassword <sftp-password> `
|
||||
-QueueId <voice-lab-queue-id> `
|
||||
-AppTokenSecret <shared-app-token-secret> `
|
||||
-Execute
|
||||
```
|
||||
|
||||
If Helm reports HPA scale field conflicts, rerun the same command with `-ForceUpgrade`.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Руководство по развертыванию (Deployment)
|
||||
|
||||
Контакт-центр поддерживает два основных сценария развертывания на серверах: упрощенный (через Docker Compose для небольших стендов) и enterprise-grade (через Kubernetes и Helm для продакшена).
|
||||
|
||||
## 1. Требования к Production-окружению
|
||||
|
||||
Для запуска в боевых условиях (Scale Profile) **строго обязателен PostgreSQL**. Запуск на SQLite в Production категорически не поддерживается.
|
||||
Также для связи микросервисов в продакшене требуется запущенный кластер **RabbitMQ**.
|
||||
|
||||
## 2. Развертывание через серверный Docker Compose (Test / Stage)
|
||||
|
||||
Если у вас один выделенный сервер (VPS/VM Linux) и вы хотите настроить автоматический деплой из GitLab:
|
||||
|
||||
Готовые скрипты находятся в `scripts/`:
|
||||
- `install_gitlab_runner.sh` — регистрирует раннер на сервере.
|
||||
- `bootstrap_gitlab_deploy.sh` — подготавливает базовые папки и .env-файлы.
|
||||
- `deploy_gitlab.sh` — сам скрипт деплоя.
|
||||
|
||||
В директории `deployment/` лежат файлы конфигураций для сервера:
|
||||
- `docker-compose.server.registry.yml` — тянет собранные образы напрямую из GitLab Container Registry.
|
||||
- `.env.production.template` — шаблон боевых секретов, включая `APP_TOKEN_SECRET` и `OIDC_...` для Keycloak.
|
||||
|
||||
**Как обновиться (CI/CD Pipeline):**
|
||||
GitLab CI (файл `.gitlab-ci.yml`) собирает Docker-образы для каждого микросервиса (`services/*`) при merge в ветку `main` и перезапускает Compose-файл на вашем выделенном сервере.
|
||||
|
||||
## 3. Развертывание в Kubernetes (On-premise / Cloud Production)
|
||||
|
||||
Для крупных внедрений контакт-центра (когда нужен автомасштаб `ai_voice_runtime_service` или `routing-service` при высокой нагрузке) используется встроенный Helm-чарт.
|
||||
|
||||
Исходники манифестов лежат в:
|
||||
- `deployment/helm/` — основной Helm Chart микросервисной платформы.
|
||||
- `deployment/kubernetes/` — статические манифесты.
|
||||
|
||||
### 3.1. Установка Helm чарта
|
||||
Для установки чарта в кластер перейдите в директорию `deployment/helm` и выполните:
|
||||
```bash
|
||||
# 1. Создание отдельного namespace
|
||||
kubectl create namespace mvp-cc
|
||||
|
||||
# 2. Установка/обновление платформы
|
||||
helm upgrade --install mvp-cc-prod ./ \
|
||||
--namespace mvp-cc \
|
||||
-f values.yaml \
|
||||
-f values.prod.yaml
|
||||
```
|
||||
|
||||
### 3.2. Масштабирование (Scale Profiles)
|
||||
Шаблоны Helm поддерживают включение профилей нагрузки. Например, для запуска `scale500` (профиль на 500 одновременных линий/пользователей):
|
||||
- Отключите встроенные SQLite-заглушки во `values.yaml`.
|
||||
- Строго пропишите внешний `DATABASE_URL` до высокодоступного кластера PostgreSQL.
|
||||
- Настройте пулинг коннектов:
|
||||
- `DB_POOL_SIZE`
|
||||
- `DB_MAX_OVERFLOW`
|
||||
|
||||
## 4. Резервное копирование и Откат конфигураций
|
||||
|
||||
Даже при работе в Kubernetes базу данных (PostgreSQL) желательно держать вне кластера (Managed Database/Patroni).
|
||||
|
||||
**Бэкапы аудиозаписей:**
|
||||
Если вы используете `recording-service` с локальным хранением, смонтированные PVC (Persistent Volume Claims) с аудиозаписями нужно регулярно копировать. В инфраструктуре без K8s для этого предусмотрен скрипт `scripts/backup_data.ps1`.
|
||||
@@ -1,83 +0,0 @@
|
||||
# Runbook - Event Bus (Track 8, Local)
|
||||
|
||||
## Goal
|
||||
|
||||
Run the first RabbitMQ-backed event bus flow locally and verify:
|
||||
|
||||
- outbox writes happen in business services
|
||||
- `event-bus-service` publishes events
|
||||
- `audit-service` consumes them
|
||||
- `reporting-service` consumes them
|
||||
|
||||
## Local compose path
|
||||
|
||||
Use Docker Compose because it includes RabbitMQ:
|
||||
|
||||
```powershell
|
||||
cd deployment
|
||||
docker compose up -d
|
||||
cd ..
|
||||
python scripts\migrate_core_db.py
|
||||
```
|
||||
|
||||
RabbitMQ management UI:
|
||||
|
||||
- `http://localhost:15672`
|
||||
- default local credentials: `guest / guest`
|
||||
|
||||
## Required env
|
||||
|
||||
The compose profile already sets:
|
||||
|
||||
- `EVENT_BUS_ENABLED=1`
|
||||
- `EVENT_BUS_URL=amqp://guest:guest@rabbitmq:5672/`
|
||||
- `EVENT_BUS_CONSUMER_ENABLED=1`
|
||||
|
||||
For non-compose local runs, export them manually before starting services.
|
||||
|
||||
## Smoke check
|
||||
|
||||
```powershell
|
||||
python scripts\event_bus_smoke.py --base-url http://localhost:8080
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- gateway and `event-bus-service` are healthy
|
||||
- an interaction is created
|
||||
- its outbox event becomes `published`
|
||||
- `audit-service` processes the event
|
||||
- `reporting-service` processes the event
|
||||
|
||||
## Inspect outbox
|
||||
|
||||
```powershell
|
||||
curl http://localhost:8080/proxy/event-bus/bus/outbox -H "X-User: admin" -H "X-Role: admin"
|
||||
```
|
||||
|
||||
Useful statuses:
|
||||
|
||||
- `pending`
|
||||
- `published`
|
||||
- `failed`
|
||||
|
||||
## Retry a failed event
|
||||
|
||||
```powershell
|
||||
curl -X POST http://localhost:8080/proxy/event-bus/bus/outbox/<event_id>/retry -H "X-User: admin" -H "X-Role: admin"
|
||||
```
|
||||
|
||||
## Acceptance check
|
||||
|
||||
```powershell
|
||||
python scripts\track8_check.py --base-url http://localhost:8080
|
||||
```
|
||||
|
||||
## Compatibility mode
|
||||
|
||||
If you want to disable the bus and keep the platform on pure HTTP-only behavior:
|
||||
|
||||
- set `EVENT_BUS_ENABLED=0`
|
||||
- restart services
|
||||
|
||||
The business REST APIs keep working in that mode.
|
||||
@@ -1,80 +0,0 @@
|
||||
# GitLab CI/CD for call-center
|
||||
|
||||
## What this setup does
|
||||
|
||||
- a push to `main` triggers GitLab CI
|
||||
- the job runs on a dedicated `shell` runner with tag `call-center-prod`
|
||||
- the runner syncs the repository into `/home/gitlab-runner/deploy/call-center`
|
||||
- Docker builds `call-center-app:<commit-sha>` and also tags `call-center-app:latest`
|
||||
- `docker compose` recreates the stack from `deployment/docker-compose.server.yml`
|
||||
- the public gateway container is named `call-center-app`
|
||||
|
||||
## Required server prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
- outbound access to `https://gitlab.konturai.kz`
|
||||
- runner token from GitLab with prefix `glrt-...` or a valid project/group runner token
|
||||
|
||||
## Install and register the runner
|
||||
|
||||
Run on the target server as `root`:
|
||||
|
||||
```bash
|
||||
cd /path/to/call-center
|
||||
RUNNER_TOKEN=glrt-xxxxxxxx bash scripts/install_gitlab_runner.sh
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
- installs `gitlab-runner` from the official GitLab repository
|
||||
- adds user `gitlab-runner` to the `docker` group
|
||||
- creates deploy directory `/home/gitlab-runner/deploy/call-center`
|
||||
- registers runner `call-center-prod-runner` with tag `call-center-prod`
|
||||
|
||||
If you first want to install the service without registration:
|
||||
|
||||
```bash
|
||||
SKIP_REGISTER=1 bash scripts/install_gitlab_runner.sh
|
||||
```
|
||||
|
||||
## Production environment file
|
||||
|
||||
Create the production env file once on the server:
|
||||
|
||||
```bash
|
||||
install -m 600 /dev/null /home/gitlab-runner/deploy/call-center/.env.production
|
||||
```
|
||||
|
||||
Then fill it with the values required by `deployment/docker-compose.server.yml`.
|
||||
|
||||
If the project is already running from `/root/call-center`, migrate the current env file and SQLite/files before the first CI deploy:
|
||||
|
||||
```bash
|
||||
bash scripts/bootstrap_gitlab_deploy.sh
|
||||
```
|
||||
|
||||
Alternative:
|
||||
|
||||
- keep the env file elsewhere
|
||||
- pass `DEPLOY_ENV_FILE=/absolute/path/to/.env.production` in GitLab CI/CD variables
|
||||
|
||||
## GitLab CI/CD variables
|
||||
|
||||
Optional project variables:
|
||||
|
||||
- `DEPLOY_DIR` if you want a different deploy directory
|
||||
- `APP_IMAGE_NAME` if you want a different Docker image name
|
||||
- `HEALTHCHECK_URL` if the gateway health URL differs
|
||||
- `DEPLOY_ENV_FILE` if `.env.production` should be copied from another location
|
||||
|
||||
## First deployment
|
||||
|
||||
1. Register the runner.
|
||||
2. Add the production env file.
|
||||
3. Push this configuration to the `main` branch.
|
||||
4. Confirm the pipeline completes successfully.
|
||||
5. Verify the container:
|
||||
|
||||
```bash
|
||||
docker ps --filter name=call-center-app
|
||||
```
|
||||
@@ -1,86 +0,0 @@
|
||||
# GitLab -> Server Deploy
|
||||
|
||||
This project is prepared for a GitLab-first delivery flow:
|
||||
|
||||
1. Push source code to GitLab.
|
||||
2. GitLab CI runs tests.
|
||||
3. GitLab CI builds and pushes container images to GitLab Container Registry.
|
||||
4. The server only receives the deployment bundle and pulls images from the registry.
|
||||
|
||||
For a same-host shell-runner flow that rebuilds `call-center-app` directly on push to `main`, see:
|
||||
|
||||
- `docs/runbooks/gitlab-cicd.md`
|
||||
|
||||
## Files used
|
||||
|
||||
- `.gitlab-ci.yml`
|
||||
- `deployment/docker-compose.server.registry.yml`
|
||||
- `deployment/docker-compose.asterisk.server.registry.yml`
|
||||
- `deployment/.env.images.example`
|
||||
|
||||
## GitLab CI variables
|
||||
|
||||
Required for image publishing:
|
||||
|
||||
- `CI_REGISTRY`
|
||||
- `CI_REGISTRY_USER`
|
||||
- `CI_REGISTRY_PASSWORD`
|
||||
- `CI_REGISTRY_IMAGE`
|
||||
|
||||
Required for the optional deploy job:
|
||||
|
||||
- `DEPLOY_HOST`
|
||||
- `DEPLOY_USER`
|
||||
- `DEPLOY_PATH`
|
||||
- `DEPLOY_SSH_PRIVATE_KEY`
|
||||
|
||||
`DEPLOY_PATH` should point to a minimal runtime directory on the server, for example:
|
||||
|
||||
```text
|
||||
/opt/call-center
|
||||
```
|
||||
|
||||
The server runtime directory should contain:
|
||||
|
||||
- `.env.production`
|
||||
- `.data_local/`
|
||||
- `.asterisk_assets/` (can stay empty)
|
||||
- `deployment/`
|
||||
|
||||
## Server deploy bundle
|
||||
|
||||
The server no longer needs the full project checkout. It only needs:
|
||||
|
||||
- `deployment/`
|
||||
- `.env.production`
|
||||
- `.data_local/`
|
||||
- `.asterisk_assets/` if you use custom prompt files
|
||||
|
||||
The app image is resolved through `APP_IMAGE`.
|
||||
The Asterisk image is resolved through `ASTERISK_IMAGE`.
|
||||
|
||||
Store them in `deployment/.env.images`, for example:
|
||||
|
||||
```text
|
||||
APP_IMAGE=registry.gitlab.example.com/group/project/app:<tag>
|
||||
ASTERISK_IMAGE=registry.gitlab.example.com/group/project/asterisk:<tag>
|
||||
```
|
||||
|
||||
## Manual server deploy
|
||||
|
||||
From the server:
|
||||
|
||||
```bash
|
||||
cd /opt/call-center/deployment
|
||||
docker login <registry>
|
||||
docker compose --env-file .env.images -f docker-compose.server.registry.yml pull
|
||||
docker compose --env-file .env.images -f docker-compose.server.registry.yml up -d
|
||||
docker compose --env-file .env.images -f docker-compose.asterisk.server.registry.yml pull
|
||||
docker compose --env-file .env.images -f docker-compose.asterisk.server.registry.yml up -d
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `deployment/docker-compose.server.yml` is still the source-build variant.
|
||||
- `deployment/docker-compose.server.registry.yml` is the registry/pull-only variant for the server.
|
||||
- The deploy job in `.gitlab-ci.yml` is manual on the default branch by design.
|
||||
@@ -1,102 +0,0 @@
|
||||
# IVR Local Runbook
|
||||
|
||||
Use this runbook to validate both the preview IVR runtime and the live Asterisk-backed IVR staging path.
|
||||
|
||||
## What Track 5 adds
|
||||
|
||||
- `ivr-service` for IVR flow CRUD and DTMF runtime sessions
|
||||
- one active IVR flow per voice queue
|
||||
- `routing-service` support for `ivr_session_id`
|
||||
- admin-side IVR preview flow in `/admin`
|
||||
|
||||
## Preview baseline vs live IVR
|
||||
|
||||
- Preview baseline: use the `/admin` IVR block to create flows, start sessions, send DTMF, and preview route overrides without telephony.
|
||||
- Live Asterisk IVR: call `7200` on the lab PBX after `asterisk-bridge-service` is configured with `ASTERISK_IVR_FASTAGI_*` and the flow nodes have valid `prompt_audio_key` values.
|
||||
|
||||
## Create a flow
|
||||
|
||||
1. Open `http://localhost:8080/admin`
|
||||
2. Go to the `IVR` block
|
||||
3. Set:
|
||||
- `queue_id`
|
||||
- `entry_node_id` (`root`)
|
||||
- `flow_json`
|
||||
4. Click `Create flow`
|
||||
|
||||
The flow JSON is validated before it is saved.
|
||||
|
||||
## Start a session
|
||||
|
||||
1. In the same `IVR` block, set:
|
||||
- `call_id`
|
||||
- `queue_id`
|
||||
- optional `interaction_id`
|
||||
2. Click `Start session`
|
||||
|
||||
The response returns:
|
||||
|
||||
- `session_id`
|
||||
- `current_node_id`
|
||||
- current node prompt metadata
|
||||
|
||||
## Send DTMF
|
||||
|
||||
1. Enter a single digit (`0`-`9`)
|
||||
2. Click `Send DTMF`
|
||||
|
||||
If the transition reaches a terminal node:
|
||||
|
||||
- the session becomes `completed`
|
||||
- `outcome_code` is set
|
||||
- `resolved_queue_id` is set
|
||||
- `resolved_queue_code` is set
|
||||
- a `voice_event` with `event_type="ivr.completed"` is written
|
||||
|
||||
## Preview route override
|
||||
|
||||
1. Keep the completed `session_id`
|
||||
2. Click `Preview IVR route`
|
||||
|
||||
This calls `routing-service` with `ivr_session_id` and shows:
|
||||
|
||||
- `original_queue_id`
|
||||
- `resolved_queue_id`
|
||||
- `ivr_outcome_code`
|
||||
|
||||
## Demo seed
|
||||
|
||||
`scripts/prepare_demo.ps1` now seeds:
|
||||
|
||||
- one demo IVR flow
|
||||
- one completed demo IVR session
|
||||
- one route preview stored in `.local_stack/demo-seed-summary.json`
|
||||
|
||||
Backstage fields:
|
||||
|
||||
- `ivr_flow_id`
|
||||
- `ivr_session_id`
|
||||
- `ivr_outcome_code`
|
||||
- `ivr_resolved_queue_id`
|
||||
|
||||
## Live Asterisk smoke
|
||||
|
||||
1. Enable:
|
||||
- `ASTERISK_BRIDGE_ENABLED=1`
|
||||
- `ASTERISK_IVR_FASTAGI_ENABLED=1`
|
||||
- `IVR_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:asterisk-bridge`
|
||||
2. Make sure `ASTERISK_QUEUE_MAP_JSON` contains `voice_lab_ivr`.
|
||||
3. Provision the `prompt_audio_key` sound files in Asterisk.
|
||||
4. Call `7200` and verify:
|
||||
- DTMF completes the IVR and transfers through `mvpcc-transfer`
|
||||
- silence triggers `ivr.step.no_input`
|
||||
- retry exhaustion abandons the IVR session and falls back to the human baseline
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- `404 No active IVR flow for queue`
|
||||
- create or activate a flow for the target queue first
|
||||
- `400 Digit must be a single character 0-9`
|
||||
- send exactly one DTMF digit
|
||||
- `400 IVR session is not completed` on route preview
|
||||
- finish the session with a terminal node before previewing routing
|
||||
@@ -1,128 +0,0 @@
|
||||
# Runbook - Keycloak OIDC (Wave 2 Track 1)
|
||||
|
||||
## Scope
|
||||
|
||||
This runbook covers the enterprise identity baseline introduced in Wave 2 Track 1:
|
||||
|
||||
- Keycloak-backed OIDC login
|
||||
- signed application bearer tokens
|
||||
- controlled fallback to local login
|
||||
|
||||
It is for QA and enterprise-like environments, not the default demo flow.
|
||||
|
||||
## Modes
|
||||
|
||||
### Local / demo
|
||||
|
||||
- `OIDC_ENABLED=0`
|
||||
- `ALLOW_LEGACY_HEADER_AUTH=1`
|
||||
|
||||
Use this when:
|
||||
|
||||
- running local demos
|
||||
- using seeded local users
|
||||
- preserving existing Stage 1-4 scripts
|
||||
|
||||
### QA transition
|
||||
|
||||
- `OIDC_ENABLED=1`
|
||||
- `ALLOW_LEGACY_HEADER_AUTH=1`
|
||||
|
||||
Use this when:
|
||||
|
||||
- validating OIDC without breaking old header-based scripts
|
||||
- checking dual-mode compatibility before cutover
|
||||
|
||||
### Enterprise pilot
|
||||
|
||||
- `OIDC_ENABLED=1`
|
||||
- `ALLOW_LEGACY_HEADER_AUTH=0`
|
||||
|
||||
Use this when:
|
||||
|
||||
- OIDC is the primary login path
|
||||
- bearer-token validation is enforced end-to-end
|
||||
|
||||
## Required configuration
|
||||
|
||||
- `APP_TOKEN_SECRET`
|
||||
- `APP_TOKEN_TTL_SECONDS` (default `3600`)
|
||||
- `OIDC_ENABLED`
|
||||
- `OIDC_PROVIDER=keycloak`
|
||||
- `OIDC_ISSUER_URL`
|
||||
- `OIDC_CLIENT_ID`
|
||||
- `OIDC_CLIENT_SECRET`
|
||||
- `OIDC_REDIRECT_URI`
|
||||
- `OIDC_SCOPES` (default `openid profile email`)
|
||||
- `OIDC_ROLE_CLAIM` (default `groups`)
|
||||
- `OIDC_ROLE_MAP_JSON`
|
||||
- `ALLOW_LEGACY_HEADER_AUTH`
|
||||
|
||||
Default role map:
|
||||
|
||||
```json
|
||||
{
|
||||
"kc_admin": "admin",
|
||||
"kc_supervisor": "supervisor",
|
||||
"kc_operator": "operator",
|
||||
"kc_analyst": "analyst"
|
||||
}
|
||||
```
|
||||
|
||||
## Keycloak client settings
|
||||
|
||||
Recommended client type:
|
||||
|
||||
- confidential client
|
||||
- standard authorization code flow enabled
|
||||
- PKCE enabled
|
||||
|
||||
Redirect URI:
|
||||
|
||||
- `https://<gateway-host>/proxy/auth/auth/oidc/callback`
|
||||
|
||||
Web origins:
|
||||
|
||||
- `https://<gateway-host>`
|
||||
|
||||
## Validation flow
|
||||
|
||||
1. Apply migrations:
|
||||
- `python scripts/migrate_core_db.py`
|
||||
2. Start the stack with Track 1 env vars.
|
||||
3. Check OIDC endpoints:
|
||||
- `python scripts/oidc_smoke.py --base-url http://<gateway-host> --require-enabled`
|
||||
4. Open:
|
||||
- `http://<gateway-host>/operator`
|
||||
5. Click `Корпоративный вход`.
|
||||
6. Complete Keycloak login.
|
||||
7. Confirm the operator UI receives a session and continues using bearer auth.
|
||||
|
||||
## Failure handling
|
||||
|
||||
### OIDC health fails
|
||||
|
||||
- verify `OIDC_ISSUER_URL`
|
||||
- verify Keycloak realm is reachable from the auth-service
|
||||
- verify TLS and reverse proxy settings
|
||||
|
||||
### Login returns `403`
|
||||
|
||||
- verify user groups in Keycloak
|
||||
- verify `OIDC_ROLE_MAP_JSON`
|
||||
- verify `OIDC_ROLE_CLAIM` matches the actual claim in the ID token
|
||||
|
||||
### Existing scripts stop working
|
||||
|
||||
- in QA only, temporarily set `ALLOW_LEGACY_HEADER_AUTH=1`
|
||||
- in enterprise pilot, do not revert silently; update the script/client to bearer auth or use documented break-glass local login only
|
||||
|
||||
## Break-glass local access
|
||||
|
||||
Local `/auth/login` remains available in Track 1 for:
|
||||
|
||||
- local development
|
||||
- demo mode
|
||||
- break-glass admin access
|
||||
|
||||
It is not the primary path in enterprise mode.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Load Test Plan (Stage 4)
|
||||
|
||||
## Target
|
||||
- 100 concurrent voice sessions
|
||||
- 100 concurrent digital sessions
|
||||
- Script entrypoint:
|
||||
- `python scripts/load_test.py --base-url http://localhost:8080 --voice 100 --digital 100`
|
||||
- or the profile-based equivalent:
|
||||
- `python scripts/load_test.py --base-url http://localhost:8080 --profile baseline_100_100`
|
||||
|
||||
## Metrics
|
||||
- API error rate
|
||||
- P95 response latency
|
||||
- queue processing delay
|
||||
- service availability during load
|
||||
|
||||
## Success criteria
|
||||
- No critical errors
|
||||
- Stable response times under threshold
|
||||
- No data loss in interaction lifecycle
|
||||
@@ -1,65 +0,0 @@
|
||||
# Runbook - Local Startup
|
||||
|
||||
1. Install dependencies:
|
||||
- `python -m pip install -r requirements.txt`
|
||||
2. Run tests:
|
||||
- `pytest -q`
|
||||
3. SQLite local stack:
|
||||
- `python scripts/local_stack.py start`
|
||||
4. PostgreSQL local stack:
|
||||
- start PostgreSQL and RabbitMQ locally
|
||||
- `python scripts/postgres_dev_preflight.py --env-file .env.postgres.local.template`
|
||||
- `python scripts/migrate_core_db.py`
|
||||
- `python scripts/local_stack.py start --env-file .env.postgres.local.template`
|
||||
- `python scripts/postgres_dev_preflight.py --env-file .env.postgres.local.template --base-url http://127.0.0.1:8080`
|
||||
5. Gateway URL:
|
||||
- `http://localhost:8080`
|
||||
6. Check health:
|
||||
- `GET /proxy/auth/health`
|
||||
7. Run Gate 1 quick check:
|
||||
- `python scripts/gate1_check.py`
|
||||
8. Run live Gate 1/2 smoke with auto-started services:
|
||||
- `python scripts/live_smoke_gate12.py`
|
||||
- PostgreSQL mode: `python scripts/live_smoke_gate12.py --database-url postgresql://mvp:mvp@localhost:5432/mvpcc`
|
||||
9. Run Gate 3 check:
|
||||
- Existing running stack: `python scripts/gate3_check.py`
|
||||
- Auto-start local stack: `python scripts/gate3_check.py --auto-start`
|
||||
10. Run Gate 4 automated checks:
|
||||
- `python scripts/gate4_check.py`
|
||||
- Keep artifacts: `python scripts/gate4_check.py --keep-artifacts`
|
||||
11. Run UAT preflight:
|
||||
- Existing running stack: `python scripts/uat_preflight.py --base-url http://localhost:8080`
|
||||
- Auto-start local stack: `python scripts/uat_preflight.py --auto-start`
|
||||
12. Run UAT dry-run package:
|
||||
- Existing running stack: `python scripts/uat_dry_run.py --base-url http://localhost:8080 --update-defect-register`
|
||||
- Auto-start local stack: `python scripts/uat_dry_run.py --auto-start --update-defect-register`
|
||||
13. Optional OIDC smoke-check:
|
||||
- `python scripts/oidc_smoke.py --base-url http://localhost:8080`
|
||||
- Require enterprise path enabled: `python scripts/oidc_smoke.py --base-url http://localhost:8080 --require-enabled`
|
||||
14. Open operator UI:
|
||||
- `http://localhost:8080/operator`
|
||||
15. Optional dedicated Wave 2 shells:
|
||||
- `http://localhost:8080/supervisor`
|
||||
- `http://localhost:8080/admin`
|
||||
16. Reset local runtime artifacts when needed:
|
||||
- `powershell -ExecutionPolicy Bypass -File scripts\clean_workspace.ps1`
|
||||
|
||||
## Runtime artifacts
|
||||
|
||||
- `.data*`, `.local_stack`, and `.artifacts` are regeneratable local runtime directories.
|
||||
- PostgreSQL local profile keeps runtime artifacts in `.data_local_pg` by default.
|
||||
- Use `scripts/clean_workspace.ps1` to clear local DBs, logs, cached recordings, and test/runtime leftovers without touching source files or backups.
|
||||
|
||||
## Local auth modes
|
||||
|
||||
- Demo/dev default:
|
||||
- `OIDC_ENABLED=0`
|
||||
- `ALLOW_LEGACY_HEADER_AUTH=1`
|
||||
- Enterprise-like local validation:
|
||||
- set `OIDC_ENABLED=1`
|
||||
- set `ALLOW_LEGACY_HEADER_AUTH=0`
|
||||
- configure `APP_TOKEN_SECRET`, `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_REDIRECT_URI`
|
||||
|
||||
See also:
|
||||
|
||||
- [keycloak-oidc.md](/e:/Zhan/docs/runbooks/keycloak-oidc.md)
|
||||
@@ -0,0 +1,94 @@
|
||||
# Руководство по локальному запуску (Local Setup)
|
||||
|
||||
В этом документе описаны шаги для поднятия проекта на рабочей машине разработчика или для проведения презентации (демо). Проект поддерживает как легковесный запуск на SQLite, так и масштабируемый запуск с использованием PostgreSQL и RabbitMQ.
|
||||
|
||||
## 1. Системные требования
|
||||
- **Python 3.10+** (для микросервисов)
|
||||
- **Docker и Docker Compose** (для запуска PostgreSQL, RabbitMQ и т.д.)
|
||||
- **PowerShell** (основные launch-скрипты написаны на нём)
|
||||
|
||||
## 2. Быстрый старт: Режим ДЕМО (Автоматически)
|
||||
|
||||
Самый удобный способ поднять весь стек контакт-центра одной командой. Скрипт запустит сервисы, прогонит смоук-тесты и заполнит базу моковыми данными:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\prepare_demo.ps1
|
||||
```
|
||||
|
||||
**Что делает `prepare_demo.ps1`**:
|
||||
- Поднимает все микросервисы проекта в фоне (логи складываются в папку `.local_stack\logs`).
|
||||
- Создает тестового клиента и историю его взаимодействия.
|
||||
- Симулирует звонок с IVR и создает запись в `recording-service`.
|
||||
- Наливает статьи Базы Знаний.
|
||||
- Забрасывает метрики для KPI отчетов в `reporting-service`.
|
||||
|
||||
**Остановка и очистка:**
|
||||
```powershell
|
||||
# Остановить все фоновые процессы сервисов
|
||||
powershell -ExecutionPolicy Bypass -File scripts\stop_all_local.ps1
|
||||
|
||||
# Полностью очистить рабочее пространство сервера (удалить временные БД и артефакты)
|
||||
powershell -ExecutionPolicy Bypass -File scripts\clean_workspace.ps1
|
||||
```
|
||||
|
||||
## 3. Ручной запуск (Local Development)
|
||||
|
||||
Если вы разрабатываете конкретный микросервис и вам нужен горячий рестарт (Hot Reload), поднимайте сервисы вручную.
|
||||
|
||||
### Шаг 3.1: Установка зависимостей
|
||||
```bash
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Шаг 3.2: Запуск инфраструктуры (Docker)
|
||||
Если вы не хотите использовать fallback-режим с SQLite, поднимите PostgreSQL и системные сервисы:
|
||||
```bash
|
||||
cd deployment
|
||||
docker compose up -d postgres
|
||||
cd ..
|
||||
```
|
||||
|
||||
### Шаг 3.3: Миграции Базы Данных
|
||||
Перед запуском бизнес-логики необходимо проинициализировать схему БД:
|
||||
```bash
|
||||
python scripts/migrate_core_db.py
|
||||
```
|
||||
|
||||
### Шаг 3.4: Запуск Gateway и Микросервисов
|
||||
Для маршрутизации запросов обязательно должен работать `api-gateway`:
|
||||
```bash
|
||||
uvicorn gateway.app:app --reload --port 8080
|
||||
```
|
||||
Далее в новых консолях вы можете точечно поднимать необходимые вам микросервисы (по умолчанию Gateway проксирует запросы на порты `8001`, `8004` и т.д.):
|
||||
```bash
|
||||
uvicorn services.auth_service.app:app --reload --port 8001
|
||||
uvicorn services.interaction_service.app:app --reload --port 8004
|
||||
```
|
||||
|
||||
## 4. Точки входа (Пользовательские интерфейсы)
|
||||
|
||||
Если `gateway.app` успешно запущен на порту 8080, вы можете попасть в веб-интерфейсы платформы по следующим адресам:
|
||||
|
||||
- **Рабочее место Оператора**: [http://localhost:8080/operator](http://localhost:8080/operator)
|
||||
- **Панель Супервизора**: [http://localhost:8080/supervisor](http://localhost:8080/supervisor)
|
||||
- **Панель Администратора**: [http://localhost:8080/admin](http://localhost:8080/admin)
|
||||
- **Аналитика (Дашборды)**: [http://localhost:8080/analyst](http://localhost:8080/analyst)
|
||||
|
||||
## 5. Полезные скрипты
|
||||
|
||||
В папке `scripts/` лежит множество полезных инструментов для разработки:
|
||||
|
||||
```bash
|
||||
# Прогон всех автотестов
|
||||
pytest -q
|
||||
|
||||
# Смоук-чек авторизации (проверка выдачи JWT токенов)
|
||||
python scripts/oidc_smoke.py --base-url http://localhost:8080
|
||||
|
||||
# Снятие и распаковка бэкапа (дамп данных лок. среды)
|
||||
powershell -ExecutionPolicy Bypass -File scripts\backup_data.ps1
|
||||
powershell -ExecutionPolicy Bypass -File scripts\restore_data.ps1 -BackupZip e:\Zhan\backups\mvp_cc_data_YYMMDD.zip
|
||||
|
||||
# Нагрузочное тестирование (позволяет сгенерировать фейковый трафик звонков)
|
||||
python scripts/load_test.py --base-url http://localhost:8080 --profile baseline_100_100
|
||||
```
|
||||
@@ -1,39 +0,0 @@
|
||||
# Management Demo Brief
|
||||
|
||||
Use this note during the meeting so the demo stays short and clear.
|
||||
|
||||
## One-minute version
|
||||
|
||||
1. Open `http://localhost:8080/operator`.
|
||||
2. Log in as `admin / admin123`.
|
||||
3. Show one closed interaction and one active interaction.
|
||||
4. Show the supervisor summary.
|
||||
5. Show the KPI summary.
|
||||
|
||||
## Three-minute version
|
||||
|
||||
1. Show the demo checklist at the top of the screen.
|
||||
2. Explain that the stand is started by one command and loaded with demo data.
|
||||
3. Open customers and interactions.
|
||||
4. Show assignment, transfer to second line, and closing.
|
||||
5. Trigger the voice, Telegram, Webchat, and Email channel checks.
|
||||
6. Search KB for `demo-showcase`.
|
||||
7. Show supervisor and KPI summary cards.
|
||||
|
||||
## What to say by block
|
||||
|
||||
- Session: role-based access and a working login flow.
|
||||
- Customers: the platform stores and finds client cards.
|
||||
- Interactions: the request can be created, assigned, escalated, and closed.
|
||||
- Integrations: the system accepts voice, Telegram, Webchat, and Email events.
|
||||
- Knowledge Base: the operator can quickly find a prepared answer.
|
||||
- Supervisor: the lead sees current queue and agent state.
|
||||
- KPI: the system already calculates core operational metrics.
|
||||
|
||||
## Five short answers
|
||||
|
||||
- What is ready: MVP with operator flow, integrations, KB, supervisor, KPI, and the first Wave 2 webchat/email channels.
|
||||
- What is not yet final: this is a pilot-ready MVP, not a full production rollout.
|
||||
- What is the stack: Python/FastAPI backend, browser UI, microservice layout.
|
||||
- What about telephony: the voice integration contour is ready and demonstrated by event intake.
|
||||
- What is next: a separate Wave 2 with SSO, extra channels, expanded admin UX, and scaling.
|
||||
@@ -1,88 +0,0 @@
|
||||
# Runbook - Pilot UAT
|
||||
|
||||
## Participants
|
||||
|
||||
- Operators
|
||||
- Supervisor
|
||||
- Analyst
|
||||
- Admin
|
||||
- Business owner
|
||||
- IT owner
|
||||
|
||||
## Scope Rules
|
||||
|
||||
- Freeze the pilot scope using `docs/gates/mvp-pilot-baseline.md`.
|
||||
- Do not record out-of-scope feature requests as MVP defects.
|
||||
- Route deferred requests to `docs/roadmap/05-wave2-backlog.md`.
|
||||
|
||||
## Preparation
|
||||
|
||||
1. Run environment preflight:
|
||||
- Existing environment: `python scripts/uat_preflight.py --base-url http://<gateway-host>:8080`
|
||||
- Local auto-start: `python scripts/uat_preflight.py --auto-start`
|
||||
2. Run UAT dry-run package generator:
|
||||
- Existing environment: `python scripts/uat_dry_run.py --base-url http://<gateway-host>:8080 --update-defect-register`
|
||||
- Local auto-start: `python scripts/uat_dry_run.py --auto-start --update-defect-register`
|
||||
3. Create session protocol from template or generated artifact:
|
||||
- `docs/uat/session-template.md`
|
||||
- Recommended bundle generator:
|
||||
- `python scripts/uat_manual_prepare.py --environment-url http://<pilot-gateway>:8080`
|
||||
4. Prepare defect log file:
|
||||
- `docs/uat/defect-log-template.csv`
|
||||
5. Confirm the pilot gateway URL and the build under test in the session protocol.
|
||||
|
||||
## Mandatory scenarios
|
||||
|
||||
- Execute all scenarios from:
|
||||
- `docs/uat/scenario-checklist.md`
|
||||
- Required scope:
|
||||
- auth login and RBAC deny
|
||||
- customer create/search
|
||||
- voice lifecycle
|
||||
- assignment and escalation
|
||||
- routing and timeline verification
|
||||
- voice event intake
|
||||
- Telegram lifecycle
|
||||
- supervisor realtime
|
||||
- KPI report and CSV
|
||||
- KB usage in handling flow
|
||||
- backup/restore verification
|
||||
- load test baseline (`100 voice + 100 digital`)
|
||||
|
||||
## Defect triage
|
||||
|
||||
- `P1`: acceptance impossible, fix in MVP immediately
|
||||
- `P2`: key journey broken, fix in MVP immediately
|
||||
- `P3`: acceptable with limitation, defer unless it blocks sign-off
|
||||
- `P4`: cosmetic or backlog-only
|
||||
|
||||
Only `P1` and `P2` defects belong to the MVP remediation cycle.
|
||||
|
||||
## Evidence package
|
||||
|
||||
- Preflight report from `docs/uat/evidence/`
|
||||
- Completed scenario checklist
|
||||
- Completed session protocol
|
||||
- Filled defect log
|
||||
- Signed sheet from `docs/uat/signoff-template.md`
|
||||
- Updated `docs/gates/p1-p2-defects.md`
|
||||
|
||||
## Formal closure
|
||||
|
||||
After manual signatures are collected and no open `P1/P2` items remain:
|
||||
|
||||
- Optional validation only:
|
||||
- `python scripts/finalize_mvp_pilot.py --session-dir docs/uat/evidence/manual_<session-id> --dry-run`
|
||||
- Run:
|
||||
- `python scripts/finalize_mvp_pilot.py --session-dir docs/uat/evidence/manual_<session-id>`
|
||||
- This updates:
|
||||
- `docs/gates/gate-04-pilot-hardening.md`
|
||||
- `docs/gates/p1-p2-defects.md`
|
||||
- `docs/releases/v1.0.0-mvp-accepted.md`
|
||||
|
||||
## Exit criteria
|
||||
|
||||
- No open P1 defects
|
||||
- No open P2 defects
|
||||
- Critical user journeys approved
|
||||
- Sign-off captured from business and IT owners
|
||||
@@ -1,275 +0,0 @@
|
||||
# PostgreSQL Dev Cutover
|
||||
|
||||
Этот runbook описывает безопасный перевод `dev`-окружения с SQLite на PostgreSQL без переноса старых данных.
|
||||
|
||||
## Что считается "без простоя"
|
||||
|
||||
В этом проекте без переноса SQLite-данных безостановочный cutover означает не hot-swap одной и той же базы, а:
|
||||
|
||||
1. поднять новый PostgreSQL-backed stack параллельно;
|
||||
2. прогнать smoke на новом stack;
|
||||
3. переключить dev-трафик на новый gateway;
|
||||
4. оставить старый SQLite stack живым до подтверждения GO;
|
||||
5. при проблеме вернуть трафик обратно без восстановления данных.
|
||||
|
||||
Если сейчас у dev только один instance и нет возможности держать parallel stack, нужен короткий maintenance window.
|
||||
|
||||
## Scope
|
||||
|
||||
Включает:
|
||||
|
||||
- подготовку PostgreSQL и RabbitMQ;
|
||||
- применение SQL migrations;
|
||||
- запуск сервисов в `SCHEMA_MANAGEMENT_MODE=migrations`;
|
||||
- smoke-проверки до и после переключения;
|
||||
- rollback на старый SQLite-backed dev stack.
|
||||
|
||||
Не включает:
|
||||
|
||||
- перенос данных из SQLite;
|
||||
- cleanup legacy SQLite-path;
|
||||
- production cutover.
|
||||
|
||||
## Required Inputs
|
||||
|
||||
- `DEV_GATEWAY_OLD`
|
||||
- `DEV_GATEWAY_NEW`
|
||||
- `POSTGRES_DATABASE_URL`
|
||||
- `APP_TOKEN_SECRET`
|
||||
- `PUBLIC_SWITCH_METHOD`
|
||||
- DNS
|
||||
- reverse proxy
|
||||
- load balancer route
|
||||
- `RABBITMQ_URL`
|
||||
|
||||
Рекомендуемые значения для локально-управляемого dev:
|
||||
|
||||
- `POSTGRES_DATABASE_URL=postgresql://mvp:mvp@<postgres-host>:5432/mvpcc`
|
||||
- `SCHEMA_MANAGEMENT_MODE=migrations`
|
||||
- env template: [\.env.postgres.local.template](/e:/Zhan/.env.postgres.local.template)
|
||||
|
||||
## Phase 0 - Preconditions
|
||||
|
||||
Подтвердить перед началом:
|
||||
|
||||
1. Текущий SQLite-backed dev stack стабилен.
|
||||
2. Перенос данных не нужен.
|
||||
3. PostgreSQL доступен по сети с host, где запускаются сервисы.
|
||||
4. RabbitMQ доступен по сети, если в dev нужен `EVENT_BUS_ENABLED=1`.
|
||||
5. Новая dev-конфигурация использует:
|
||||
- `DATABASE_URL=postgresql://...`
|
||||
- `SCHEMA_MANAGEMENT_MODE=migrations`
|
||||
6. Старый SQLite stack не останавливается до завершения smoke на новом stack.
|
||||
|
||||
## Phase 1 - Preflight
|
||||
|
||||
На будущем PostgreSQL-backed dev host:
|
||||
|
||||
1. Проверить Python зависимости:
|
||||
|
||||
```powershell
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Подготовить env-файл:
|
||||
- скопировать [\.env.postgres.local.template](/e:/Zhan/.env.postgres.local.template)
|
||||
- заполнить `DATABASE_URL`, `APP_TOKEN_SECRET`, интеграционные URL и при необходимости AI/Telegram/WhatsApp переменные
|
||||
|
||||
3. Запустить DB-only preflight:
|
||||
|
||||
```powershell
|
||||
python scripts\postgres_dev_preflight.py --env-file .env.postgres.local.template
|
||||
```
|
||||
|
||||
4. Убедиться, что mode не legacy:
|
||||
|
||||
```text
|
||||
SCHEMA_MANAGEMENT_MODE=migrations
|
||||
```
|
||||
|
||||
5. Применить миграции в PostgreSQL:
|
||||
|
||||
```powershell
|
||||
python scripts\migrate_core_db.py
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
|
||||
- миграции применились без ошибок;
|
||||
- в PostgreSQL появилась таблица `schema_migrations`;
|
||||
- сервисы ещё не стартовали.
|
||||
|
||||
## Phase 2 - Bring Up Parallel Stack
|
||||
|
||||
Поднять новый stack параллельно старому.
|
||||
|
||||
Если запуск локальный:
|
||||
|
||||
```powershell
|
||||
python scripts\local_stack.py start `
|
||||
--runtime-dir .local_stack_pg `
|
||||
--env-file .env.postgres.local.template `
|
||||
--force-restart
|
||||
```
|
||||
|
||||
Если это dev server / VM:
|
||||
|
||||
1. развернуть те же сервисы в отдельный deployment set;
|
||||
2. прокинуть им:
|
||||
- `DATABASE_URL=postgresql://...`
|
||||
- `SCHEMA_MANAGEMENT_MODE=migrations`
|
||||
3. не направлять внешний dev-трафик на новый gateway до завершения smoke.
|
||||
|
||||
Важно:
|
||||
|
||||
- новый stack должен использовать отдельный runtime/log directory;
|
||||
- старый SQLite-backed stack остаётся доступным;
|
||||
- нельзя смешивать новый dev-трафик со старым gateway до smoke.
|
||||
|
||||
## Phase 3 - Smoke Before Switch
|
||||
|
||||
Выполнить проверки на `DEV_GATEWAY_NEW`.
|
||||
|
||||
Сначала прогнать HTTP-aware preflight:
|
||||
|
||||
```powershell
|
||||
python scripts\postgres_dev_preflight.py `
|
||||
--env-file .env.postgres.local.template `
|
||||
--base-url http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
1. Health:
|
||||
|
||||
```powershell
|
||||
python scripts\live_smoke_gate12.py --base-url http://<DEV_GATEWAY_NEW> --database-url <POSTGRES_DATABASE_URL>
|
||||
```
|
||||
|
||||
2. Focused PostgreSQL smoke:
|
||||
|
||||
```powershell
|
||||
pytest -q tests/test_postgres_readiness.py
|
||||
```
|
||||
|
||||
3. Дополнительно проверить вручную:
|
||||
|
||||
- `GET /proxy/auth/health`
|
||||
- `GET /proxy/interaction/health`
|
||||
- `GET /proxy/voice/health`
|
||||
- `GET /proxy/ai-voice-runtime/health`
|
||||
- открыть `http://<DEV_GATEWAY_NEW>/operator`
|
||||
|
||||
4. Если в dev включён event bus:
|
||||
|
||||
- проверить подключение к RabbitMQ;
|
||||
- убедиться, что `event-bus-service` поднимается в `healthy`;
|
||||
- убедиться, что новые записи появляются в `event_outbox`.
|
||||
|
||||
GO в следующую фазу только если:
|
||||
|
||||
- health green;
|
||||
- smoke green;
|
||||
- новый stack не пытается auto-create schema на старте;
|
||||
- нет ошибок вида `run python scripts/migrate_core_db.py` после фактического применения миграций.
|
||||
|
||||
## Phase 4 - Traffic Switch
|
||||
|
||||
Переключение выполняется только после успешного Phase 3.
|
||||
|
||||
Рекомендуемый порядок:
|
||||
|
||||
1. уменьшить TTL у dev DNS / подготовить proxy route заранее;
|
||||
2. переключить `DEV_GATEWAY` на `DEV_GATEWAY_NEW`;
|
||||
3. не останавливать старый SQLite stack;
|
||||
4. сразу после switch выполнить быстрый post-switch smoke.
|
||||
|
||||
Примеры post-switch smoke:
|
||||
|
||||
```powershell
|
||||
python scripts\live_smoke_gate12.py --base-url http://<DEV_GATEWAY_PUBLIC> --database-url <POSTGRES_DATABASE_URL>
|
||||
```
|
||||
|
||||
Проверить руками:
|
||||
|
||||
- логин;
|
||||
- создание interaction;
|
||||
- digital thread/message flow;
|
||||
- voice event ingest;
|
||||
- создание voice AI session.
|
||||
|
||||
## Phase 5 - Stabilization Window
|
||||
|
||||
В течение первых 15-30 минут после switch:
|
||||
|
||||
1. наблюдать логи gateway и сервисов;
|
||||
2. следить за ошибками подключения к PostgreSQL;
|
||||
3. следить за ошибками `schema mismatch`;
|
||||
4. следить за ошибками блокировок/unique conflict в voice transcript path;
|
||||
5. сравнивать user-visible поведение со старым dev stack.
|
||||
|
||||
Если всё стабильно:
|
||||
|
||||
1. объявить GO;
|
||||
2. зафиксировать, что `dev` теперь PostgreSQL-backed;
|
||||
3. оставить SQLite stack выключенным, но не удалённым до конца рабочего дня.
|
||||
|
||||
## Rollback
|
||||
|
||||
Rollback делается только через возврат трафика на старый SQLite-backed stack.
|
||||
|
||||
Триггеры rollback:
|
||||
|
||||
- новый gateway не проходит smoke;
|
||||
- сервисы падают на startup;
|
||||
- критичный API path broken;
|
||||
- UI не работает;
|
||||
- ошибки подключения к PostgreSQL не устраняются быстро;
|
||||
- event bus / voice path деградирует.
|
||||
|
||||
Шаги rollback:
|
||||
|
||||
1. вернуть `DEV_GATEWAY_PUBLIC` на `DEV_GATEWAY_OLD`;
|
||||
2. убедиться, что старый SQLite stack всё ещё жив;
|
||||
3. выполнить быстрый smoke на старом stack;
|
||||
4. запретить новый трафик на PostgreSQL-backed stack;
|
||||
5. собрать логи с нового stack.
|
||||
|
||||
Rollback успешен, если:
|
||||
|
||||
- dev UI снова открывается на старом gateway;
|
||||
- CRUD и smoke снова зелёные;
|
||||
- команда работает на старом dev без дополнительных действий.
|
||||
|
||||
## Cutover Sheet
|
||||
|
||||
Перед cutover заполнить:
|
||||
|
||||
- время старта;
|
||||
- кто выполняет switch;
|
||||
- `DEV_GATEWAY_OLD`;
|
||||
- `DEV_GATEWAY_NEW`;
|
||||
- `POSTGRES_DATABASE_URL`;
|
||||
- результат preflight;
|
||||
- результат smoke до switch;
|
||||
- время switch;
|
||||
- результат smoke после switch;
|
||||
- GO / ROLLBACK;
|
||||
- короткий список замечаний.
|
||||
|
||||
## Minimal Command Sequence
|
||||
|
||||
Если нужен самый короткий practical path для dev:
|
||||
|
||||
```powershell
|
||||
python scripts\migrate_core_db.py
|
||||
python scripts\postgres_dev_preflight.py --env-file .env.postgres.local.template
|
||||
python scripts\local_stack.py start --runtime-dir .local_stack_pg --env-file .env.postgres.local.template --force-restart
|
||||
python scripts\postgres_dev_preflight.py --env-file .env.postgres.local.template --base-url http://127.0.0.1:8080
|
||||
pytest -q tests/test_postgres_readiness.py
|
||||
python scripts\live_smoke_gate12.py --database-url postgresql://mvp:mvp@localhost:5432/mvpcc
|
||||
```
|
||||
|
||||
После этого:
|
||||
|
||||
1. переключить dev gateway/public route на новый stack;
|
||||
2. прогнать smoke ещё раз уже через публичный dev URL;
|
||||
3. оставить старый SQLite stack как rollback target.
|
||||
@@ -1,88 +0,0 @@
|
||||
# Runbook - Shared Docker Host PostgreSQL for call-center
|
||||
|
||||
Use this runbook when the server already hosts other Docker projects and `call-center` still runs on SQLite. This flow creates an isolated PostgreSQL instance for `call-center` only and does not restart the current application stack.
|
||||
|
||||
## Assets
|
||||
|
||||
- Compose file: `deployment/docker-compose.postgres.server.yml`
|
||||
- Secret env file: `/home/mvpcc/call-center/.env.postgres.server`
|
||||
- Data directory: `/home/mvpcc/call-center/.data_pg`
|
||||
- Host-local PostgreSQL port: `127.0.0.1:5434`
|
||||
|
||||
Future application connection string:
|
||||
|
||||
- containers on the same Docker host: `postgresql://mvpcc_app:<secret>@call-center-postgres:5432/mvpcc`
|
||||
- host-local checks: `postgresql://mvpcc_app:<secret>@127.0.0.1:5434/mvpcc`
|
||||
|
||||
## Bring-up
|
||||
|
||||
From the repository root on the target server:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
|
||||
target = Path(".env.postgres.server")
|
||||
target.write_text(
|
||||
"POSTGRES_DB=mvpcc\n"
|
||||
"POSTGRES_USER=mvpcc_app\n"
|
||||
f"POSTGRES_PASSWORD={secrets.token_urlsafe(32)}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
PY
|
||||
chmod 600 .env.postgres.server
|
||||
mkdir -p .data_pg
|
||||
chmod 700 .data_pg
|
||||
docker compose -f deployment/docker-compose.postgres.server.yml up -d
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Container status and logs:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center
|
||||
docker compose -f deployment/docker-compose.postgres.server.yml ps
|
||||
docker compose -f deployment/docker-compose.postgres.server.yml logs postgres --tail 50
|
||||
docker exec call-center-postgres pg_isready -U mvpcc_app -d mvpcc
|
||||
```
|
||||
|
||||
Host-local connection check without installing `psql` on the server:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center
|
||||
set -a
|
||||
. ./.env.postgres.server
|
||||
set +a
|
||||
docker run --rm --network host \
|
||||
-e PGPASSWORD="$POSTGRES_PASSWORD" \
|
||||
postgres:16-alpine \
|
||||
psql -h 127.0.0.1 -p 5434 -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
|
||||
-Atqc "select current_database(), current_user;"
|
||||
```
|
||||
|
||||
Port exposure check:
|
||||
|
||||
```bash
|
||||
ss -ltn | grep 5434
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
- the container is `healthy`
|
||||
- `pg_isready` returns `accepting connections`
|
||||
- the SQL check returns `mvpcc|mvpcc_app`
|
||||
- `ss` shows `127.0.0.1:5434`, not `0.0.0.0:5434`
|
||||
|
||||
## Scope guardrails
|
||||
|
||||
Do not do these actions in this step:
|
||||
|
||||
- do not edit `deployment/docker-compose.server.yml`
|
||||
- do not switch `DATABASE_URL` for the running `call-center` services
|
||||
- do not run application migrations against the new PostgreSQL instance yet
|
||||
- do not touch existing PostgreSQL containers used by other projects
|
||||
|
||||
When a second Dockerized `call-center` stack is added later on the same host, connect it to the external network `call-center-postgres_default` and use `call-center-postgres:5432` from the application containers.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Runbook - Parallel PostgreSQL-backed Stack on a Shared Docker Host
|
||||
|
||||
Use this runbook after the isolated PostgreSQL server from `postgres-server-docker.md` is already running. The goal is to bring up a second `call-center` stack on PostgreSQL without stopping the current SQLite-backed stack on port `8080`.
|
||||
|
||||
## Assets
|
||||
|
||||
- Compose file: `deployment/docker-compose.parallel.server.yml`
|
||||
- Base env: `/home/mvpcc/call-center-pg/.env.production`
|
||||
- Override env: `/home/mvpcc/call-center-pg/.env.postgres.parallel.server`
|
||||
- Runtime data dir: `/home/mvpcc/call-center-pg/.data_pg_parallel`
|
||||
- Gateway port: `18080`
|
||||
|
||||
This parallel stack intentionally disables active external integrations so it stays passive while the SQLite-backed stack remains live:
|
||||
|
||||
- Telegram bot disabled
|
||||
- WhatsApp outbound disabled
|
||||
- Asterisk bridge disabled
|
||||
- FastAGI disabled
|
||||
- AI voice disabled
|
||||
|
||||
The parallel stack reaches PostgreSQL over the external Docker network created by `deployment/docker-compose.postgres.server.yml`, using the hostname `call-center-postgres` on port `5432`.
|
||||
|
||||
## Server bring-up
|
||||
|
||||
Prepare a separate build workspace from the current repository checkout:
|
||||
|
||||
```bash
|
||||
rm -rf /home/mvpcc/call-center-pg
|
||||
mkdir -p /home/mvpcc/call-center-pg
|
||||
```
|
||||
|
||||
Copy the current `.env.production` from the live SQLite stack, then add a PostgreSQL override file:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center-pg
|
||||
cp /home/mvpcc/call-center/.env.production .env.production
|
||||
cp .env.postgres.parallel.server.template .env.postgres.parallel.server
|
||||
```
|
||||
|
||||
Update `.env.postgres.parallel.server`:
|
||||
|
||||
- replace `<server-host>` with the real server host or IP
|
||||
- replace the PostgreSQL password placeholder with the password from `/home/mvpcc/call-center/.env.postgres.server`
|
||||
- keep `DATABASE_URL` pointed at `call-center-postgres:5432`, not `127.0.0.1:5434`
|
||||
|
||||
Build the image and prepare the runtime directory:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center-pg
|
||||
mkdir -p .data_pg_parallel
|
||||
docker compose -f deployment/docker-compose.parallel.server.yml build
|
||||
```
|
||||
|
||||
The parallel compose builds and uses the dedicated image tag `call-center-app:pg-parallel`.
|
||||
|
||||
Run migrations before starting the stack:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center-pg
|
||||
docker compose -f deployment/docker-compose.parallel.server.yml run --rm auth-service python scripts/migrate_core_db.py
|
||||
```
|
||||
|
||||
Bring up the PostgreSQL-backed stack:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center-pg
|
||||
docker compose -f deployment/docker-compose.parallel.server.yml up -d
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Health and status:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center-pg
|
||||
docker compose -f deployment/docker-compose.parallel.server.yml ps
|
||||
curl -fsS http://127.0.0.1:18080/health
|
||||
curl -fsS http://127.0.0.1:18080/proxy/auth/health
|
||||
curl -fsS http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
Focused PostgreSQL preflight from inside the parallel stack:
|
||||
|
||||
```bash
|
||||
cd /home/mvpcc/call-center-pg
|
||||
docker compose -f deployment/docker-compose.parallel.server.yml run --rm auth-service \
|
||||
sh -lc 'python scripts/postgres_dev_preflight.py --database-url "$DATABASE_URL" --base-url http://api-gateway:8000'
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
- the new stack is reachable at `http://<server-host>:18080`
|
||||
- the old SQLite-backed stack stays reachable at `http://<server-host>:8080`
|
||||
- no host ports except `18080` are added for the parallel stack
|
||||
- the PostgreSQL-backed containers stay healthy
|
||||
- application containers talk to PostgreSQL over Docker network `call-center-postgres_default`
|
||||
|
||||
## Scope guardrails
|
||||
|
||||
Do not do these actions in this step:
|
||||
|
||||
- do not stop or restart the existing `call-center` compose project
|
||||
- do not change `/home/mvpcc/call-center/deployment/docker-compose.server.yml`
|
||||
- do not reuse the existing `call-center-app:local` image tag for the new stack
|
||||
- do not re-enable Telegram, WhatsApp, or Asterisk on the parallel stack until cutover is planned
|
||||
@@ -1,65 +0,0 @@
|
||||
# Local Recordings Runbook
|
||||
|
||||
Use this flow to validate Track 4 locally.
|
||||
|
||||
## Storage location
|
||||
|
||||
- Default root: `.data_local\recordings`
|
||||
- Override with `CC_RECORDINGS_DIR`
|
||||
|
||||
Files are copied into a managed layout:
|
||||
|
||||
- `YYYY\MM\DD\rec_<id>_<file-name>`
|
||||
|
||||
## Import flow
|
||||
|
||||
1. Submit a `recording.ready` voice event with:
|
||||
- `payload.source_path`
|
||||
- optional `file_name`, `mime_type`, `duration_seconds`, `recorded_at`
|
||||
2. Import it through:
|
||||
|
||||
```text
|
||||
POST /proxy/recording/recordings/import-from-voice-event/{event_id}
|
||||
```
|
||||
|
||||
You can also register directly by server path:
|
||||
|
||||
```text
|
||||
POST /proxy/recording/recordings/register
|
||||
```
|
||||
|
||||
## Review in supervisor shell
|
||||
|
||||
1. Open `http://localhost:8080/supervisor`
|
||||
2. Go to `Recordings`
|
||||
3. Load recordings or import by `voice_event_id`
|
||||
4. Select a row to open metadata and preview
|
||||
5. Use:
|
||||
- `Open` for metadata + playback
|
||||
- `Download` for file export
|
||||
- `Archive` to mark the recording as archived
|
||||
|
||||
## Missing file handling
|
||||
|
||||
If the DB row exists but the stored file is gone:
|
||||
|
||||
- content request returns `410 Gone`
|
||||
- the row status becomes `missing`
|
||||
- the metadata remains available for diagnosis
|
||||
|
||||
## Limits
|
||||
|
||||
- Default max size: `25 MB`
|
||||
- Override with `RECORDING_MAX_BYTES`
|
||||
|
||||
## Demo seed
|
||||
|
||||
`scripts\prepare_demo.ps1` now creates:
|
||||
|
||||
- one sample `.wav` file
|
||||
- one `recording.ready` voice event
|
||||
- one imported managed recording
|
||||
|
||||
The generated `recording_id` is stored in:
|
||||
|
||||
- `.local_stack\demo-seed-summary.json`
|
||||
@@ -1,77 +0,0 @@
|
||||
# Runbook - Track 10 Live Voice Reliability
|
||||
|
||||
Use this runbook to validate lifecycle latency after Track 9 integration.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Platform stack is running.
|
||||
- Asterisk bridge is connected (`/proxy/asterisk-bridge/asterisk/status`).
|
||||
- Real lab calls are already made (`1001 -> 7000`).
|
||||
|
||||
## 1) Generate latency report
|
||||
|
||||
```powershell
|
||||
python scripts\track10_voice_latency_report.py `
|
||||
--database-url sqlite:///.data_local/mvp_cc.db `
|
||||
--since-hours 8 `
|
||||
--breach-mode direct `
|
||||
--json-out .artifacts/track10/voice-latency-latest.json
|
||||
```
|
||||
|
||||
For post-fix short-window validation use minutes:
|
||||
|
||||
```powershell
|
||||
python scripts\track10_voice_latency_report.py `
|
||||
--database-url sqlite:///.data_local/mvp_cc.db `
|
||||
--since-minutes 30 `
|
||||
--breach-mode direct `
|
||||
--json-out .artifacts/track10/voice-latency-30m.json
|
||||
```
|
||||
|
||||
Optional strict gate mode:
|
||||
|
||||
```powershell
|
||||
python scripts\track10_voice_latency_report.py `
|
||||
--database-url sqlite:///.data_local/mvp_cc.db `
|
||||
--since-hours 8 `
|
||||
--max-started-to-ended-seconds 30 `
|
||||
--max-ended-to-recording-seconds 45 `
|
||||
--breach-mode direct `
|
||||
--fail-on-breach
|
||||
```
|
||||
|
||||
## 2) Review report fields
|
||||
|
||||
Mandatory fields:
|
||||
- `summary.total_calls`
|
||||
- `summary.active_no_end`
|
||||
- `summary.ended_no_recording_upload`
|
||||
- `summary.start_to_end.p95`
|
||||
- `summary.end_to_recording_upload.p95`
|
||||
- `summary.ended_direct_calls`
|
||||
- `summary.ended_reconciled_calls`
|
||||
- `summary.direct_start_to_end.p95`
|
||||
- `summary.direct_end_to_recording_upload.p95`
|
||||
|
||||
## 3) Save acceptance evidence
|
||||
|
||||
Create a timestamp folder:
|
||||
|
||||
`docs/acceptance/track10/<timestamp>/`
|
||||
|
||||
Place:
|
||||
- `voice-latency-latest.json`
|
||||
- command output log
|
||||
- note with latest call IDs included in report
|
||||
|
||||
## 4) Decision guidance
|
||||
|
||||
GO candidate:
|
||||
- `active_no_end = 0`
|
||||
- `ended_no_recording_upload = 0`
|
||||
- direct p95 metrics within Track 10 thresholds
|
||||
|
||||
NO-GO candidate:
|
||||
- repeated high latency outside thresholds,
|
||||
- stale active calls without `call.ended`,
|
||||
- missing recordings after completed calls.
|
||||
@@ -1,76 +0,0 @@
|
||||
# Runbook - Track 11 Live Operator Call Control
|
||||
|
||||
Use this runbook to verify the accepted call-control path introduced in Track 11.
|
||||
|
||||
For the current operator UX and active/recent call behavior, use `docs/runbooks/track12-operator-voice-ux.md`.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Track 9 bridge works: `/proxy/asterisk-bridge/asterisk/status` returns `ami_connected=true`
|
||||
- operator SIP extensions exist in Asterisk: `2001`, `2002`
|
||||
- bridge env includes:
|
||||
- `ASTERISK_CALLCONTROL_ENABLED=1`
|
||||
- `ASTERISK_OPERATOR_EXTENSION_MAP_JSON`
|
||||
- `ASTERISK_TRANSFER_TARGET_MAP_JSON`
|
||||
|
||||
## 1) Start the QA stack
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\start_track9_qa.ps1
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```powershell
|
||||
curl http://127.0.0.1:8080/proxy/asterisk-bridge/asterisk/status
|
||||
```
|
||||
|
||||
## 2) Real call scenario
|
||||
|
||||
1. Register caller softphone `1001`.
|
||||
2. Register operator softphone `2001`.
|
||||
3. Make call `1001 -> 7000`.
|
||||
4. Open `http://127.0.0.1:8080/operator` as `operator`.
|
||||
5. In `Живые звонки`:
|
||||
- select the active call
|
||||
- click `Принять в работу`
|
||||
6. Speak in `MicroSIP/Zoiper`.
|
||||
7. Finish with one of:
|
||||
- `Завершить`
|
||||
- `Передать`
|
||||
|
||||
## 3) Verify persisted events
|
||||
|
||||
Expected event types for the same call:
|
||||
- `call.started`
|
||||
- `call.connected`
|
||||
- `call.ended`
|
||||
- `recording.ready`
|
||||
- optional `call.transferred`
|
||||
|
||||
## 4) Verify recording playback
|
||||
|
||||
1. Open `http://127.0.0.1:8080/supervisor`.
|
||||
2. In `Записи`, load the latest recording for the call.
|
||||
3. Check inline playback: `audio/*`, HTTP `200`.
|
||||
|
||||
## 5) Inspect action log
|
||||
|
||||
```powershell
|
||||
curl -H "X-User: operator_a" -H "X-Role: operator" ^
|
||||
http://127.0.0.1:8080/proxy/asterisk-bridge/asterisk/live-calls/<call_id>/actions
|
||||
```
|
||||
|
||||
Each action should have:
|
||||
- `action_type` in `claim|hangup|blind-transfer`
|
||||
- `result_status=ok`
|
||||
- `ami_action_id` for a successful AMI call
|
||||
|
||||
## 6) Failure handling
|
||||
|
||||
- `403 Operator can control only own claimed call`
|
||||
- use the correct operator account or claim the call first
|
||||
- `409 Unable to resolve active channel for call`
|
||||
- check AMI permissions and channel resolution
|
||||
- `Unknown transfer queue_code`
|
||||
- fix `ASTERISK_TRANSFER_TARGET_MAP_JSON`
|
||||
@@ -1,53 +0,0 @@
|
||||
# Runbook - Track 12 Operator Voice UX
|
||||
|
||||
Use this runbook to validate the frozen operator voice baseline after Track 12.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Platform is running locally or in QA
|
||||
- `ASTERISK_CALLCONTROL_ENABLED=1`
|
||||
- operator softphone is registered: `2001` or `2002`
|
||||
- caller softphone `1001` can call `7000`
|
||||
|
||||
## Operator model
|
||||
|
||||
- The phone answer happens in `MicroSIP/Zoiper`
|
||||
- Browser action `Принять в работу` does not answer media
|
||||
- Browser actions manage ownership and call-control in the platform
|
||||
- `Активные звонки` shows only live bridge-backed calls
|
||||
- `Только что завершённые` shows short-lived recent terminal calls
|
||||
|
||||
## Happy path
|
||||
|
||||
1. Open `/operator` and log in as `operator / op12345`.
|
||||
2. Place a call `1001 -> 7000`.
|
||||
3. Answer in `MicroSIP/Zoiper`.
|
||||
4. Confirm the card appears in `Активные звонки`.
|
||||
5. Click `Принять в работу`.
|
||||
6. Confirm the card stays in `Активные звонки`.
|
||||
7. Click `Завершить`.
|
||||
8. Confirm the card moves to `Только что завершённые`.
|
||||
9. Open `/supervisor` and verify the imported recording.
|
||||
|
||||
## Transfer path
|
||||
|
||||
1. Repeat the call flow.
|
||||
2. Click `Принять в работу`.
|
||||
3. Set:
|
||||
- `target type = extension`
|
||||
- `target value = 2002`
|
||||
4. Click `Передать`.
|
||||
5. Confirm the card moves to `Только что завершённые`.
|
||||
6. Confirm the recent card shows transfer as the terminal action.
|
||||
|
||||
## Passive path
|
||||
|
||||
1. Place a call and answer it in SIP.
|
||||
2. Do not click any browser action for at least 30-40 seconds.
|
||||
3. Confirm the call stays in `Активные звонки` until the real hangup.
|
||||
|
||||
## Failure cues
|
||||
|
||||
- If `Живые звонки` shows `interaction:int_*`, the browser is using stale assets
|
||||
- If a call disappears before the real hangup, inspect bridge reconciliation behavior
|
||||
- If `Передать` succeeds in SIP but not in UI, inspect `/asterisk/live-calls/{call_id}/actions`
|
||||
@@ -1,69 +0,0 @@
|
||||
# Track 14 Browser Softphone Runbook
|
||||
|
||||
## Preconditions
|
||||
|
||||
- `ASTERISK_WEBRTC_ENABLED=1`
|
||||
- `ASTERISK_WEBRTC_WS_URL` points to the Asterisk WSS endpoint
|
||||
- `ASTERISK_BROWSER_SIP_MAP_JSON` contains a mapping for the operator app user
|
||||
- Asterisk `http.conf`, `rtp.conf`, and `pjsip.conf` from `deployment/asterisk/` are applied on the lab VM
|
||||
- The operator workstation trusts the QA certificate used by Asterisk WSS
|
||||
- Chrome or Edge is used for `/operator`
|
||||
|
||||
## Browser config contract
|
||||
|
||||
Bridge route:
|
||||
- `/proxy/asterisk-bridge/asterisk/browser-softphone/config`
|
||||
|
||||
Expected config fields:
|
||||
- `enabled`
|
||||
- `ws_url`
|
||||
- `sip_uri`
|
||||
- `authorization_username`
|
||||
- `password`
|
||||
- `display_name`
|
||||
- `ice_servers`
|
||||
- `operator_extension`
|
||||
|
||||
If the logged-in app user has no browser SIP mapping, the route returns `403`.
|
||||
|
||||
## QA startup flow
|
||||
|
||||
1. Open `/operator` in Chrome or Edge.
|
||||
2. Log in with a user that has:
|
||||
- operator role access
|
||||
- a browser SIP mapping in `ASTERISK_BROWSER_SIP_MAP_JSON`
|
||||
3. Click `Подключить browser softphone`.
|
||||
4. Allow microphone access.
|
||||
5. Confirm the `Browser Softphone` block reaches `Зарегистрирован`.
|
||||
|
||||
## Inbound QA call flow
|
||||
|
||||
1. Place a lab call from `1001` to `7000`.
|
||||
2. Wait for the incoming browser banner.
|
||||
3. Click `Answer`.
|
||||
4. The browser tries to auto-claim the live call.
|
||||
5. If auto-claim does not happen because matching is ambiguous, click `Принять в работу` manually.
|
||||
6. Click `Hang up` in the browser block or `Завершить` in the live-call block.
|
||||
7. Confirm the call moves to `Только что завершённые`.
|
||||
8. Confirm the recording is available in `/supervisor`.
|
||||
|
||||
## Coexistence notes
|
||||
|
||||
- `MicroSIP/Zoiper` remains supported in QA.
|
||||
- Browser softphone does not replace the accepted Track 11/12 control-path.
|
||||
- Browser `Hang up` still uses the accepted bridge hangup route when a live call match exists.
|
||||
|
||||
## Common failures
|
||||
|
||||
### Browser softphone config missing
|
||||
- Check `ASTERISK_BROWSER_SIP_MAP_JSON`
|
||||
- Check that the app user name matches the mapping key exactly
|
||||
|
||||
### WSS registration fails
|
||||
- Check Asterisk `http.conf`
|
||||
- Check that the workstation trusts the QA certificate
|
||||
- Check `ASTERISK_WEBRTC_WS_URL`
|
||||
|
||||
### Browser answer works but auto-claim does not
|
||||
- The current rule requires exactly one active ringing call for the operator extension
|
||||
- If more than one candidate exists, use manual `Принять в работу`
|
||||
@@ -1,44 +0,0 @@
|
||||
# Track 15 - Telegram Chat Runbook
|
||||
|
||||
## Required env
|
||||
|
||||
- `TELEGRAM_BOT_ENABLED=1`
|
||||
- `TELEGRAM_BOT_TOKEN`
|
||||
- `TELEGRAM_WEBHOOK_SECRET`
|
||||
- `TELEGRAM_DEFAULT_QUEUE_ID`
|
||||
|
||||
Optional:
|
||||
|
||||
- `TELEGRAM_BOT_API_BASE` when running against a stub or proxy
|
||||
|
||||
## Register the bot webhook
|
||||
|
||||
Use Telegram Bot API `setWebhook` with:
|
||||
|
||||
- URL -> your public `telegram-adapter-service` webhook path:
|
||||
- `/integrations/telegram/bot/webhook`
|
||||
- secret token -> `TELEGRAM_WEBHOOK_SECRET`
|
||||
|
||||
## Operator workflow
|
||||
|
||||
1. Open `/operator`
|
||||
2. Go to the `Telegram` page
|
||||
3. Wait for a real inbound bot message
|
||||
4. Select the thread from the left list
|
||||
5. Click `Принять в работу`
|
||||
6. Reply from the composer
|
||||
7. Close or escalate the thread if needed
|
||||
|
||||
## Runtime behavior
|
||||
|
||||
- one Telegram thread is stored per `chat_id`
|
||||
- new inbound messages reuse the same `thread_id` and `interaction_id`
|
||||
- if the linked interaction was `closed`, a new inbound message reactivates it to `new`
|
||||
- unsupported inbound Telegram content is stored as a visible system placeholder
|
||||
|
||||
## QA check
|
||||
|
||||
- send one first message from Telegram -> thread appears
|
||||
- send a second message from the same `chat_id` -> same thread is reused
|
||||
- operator claim works
|
||||
- operator reply appears in the thread and is sent via Bot API
|
||||
@@ -1,134 +0,0 @@
|
||||
# Runbook - Track 7 Scale Validation
|
||||
|
||||
## Goal
|
||||
|
||||
Validate the production-like `scale500` profile for Wave 2 / Track 7 on `K8s on-prem`.
|
||||
|
||||
This runbook assumes:
|
||||
|
||||
- Helm is the canonical deployment path
|
||||
- `DATABASE_URL` points to a shared PostgreSQL endpoint
|
||||
- `metrics-server` is installed and healthy
|
||||
- Helm migration hook is enabled
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `kubectl` access to the target cluster
|
||||
- `helm` installed locally
|
||||
- namespace chosen (examples below use `mvp-cc`)
|
||||
- Helm chart configured with:
|
||||
- `global.schemaManagementMode=migrations`
|
||||
- `migrations.enabled=true`
|
||||
|
||||
## Configure Helm values
|
||||
|
||||
Before deployment, confirm:
|
||||
|
||||
- `profiles.scale500.enabled=true`
|
||||
- `global.databaseUrl` is set to PostgreSQL
|
||||
- `global.schemaManagementMode=migrations`
|
||||
- `migrations.enabled=true`
|
||||
- `db.poolSize`, `db.maxOverflow`, `db.poolTimeoutSeconds`, `db.poolRecycleSeconds` match the target baseline
|
||||
|
||||
Render and inspect:
|
||||
|
||||
```powershell
|
||||
helm lint deployment\helm
|
||||
helm template mvp-cc deployment\helm -n mvp-cc
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
```powershell
|
||||
helm upgrade --install mvp-cc deployment\helm -n mvp-cc --create-namespace
|
||||
```
|
||||
|
||||
Wait until:
|
||||
|
||||
- migration Job `mvp-cc-db-migrate` completed successfully
|
||||
- all expected pods are `Ready`
|
||||
- HPA objects are created for the hot-path services
|
||||
- `recording-service` stays singleton
|
||||
|
||||
## Step validation (`250 + 250`)
|
||||
|
||||
Run the staged profile first:
|
||||
|
||||
```powershell
|
||||
python scripts\load_test.py --base-url http://<gateway-host> --profile step_250_250 --auth-mode bearer
|
||||
python scripts\track7_check.py --namespace mvp-cc --report-dir .artifacts\track7\<timestamp> --require-success-rate 99 --require-p95-seconds 1.5 --require-p99-seconds 3.0
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- success rate `>= 99%`
|
||||
- no crash loops or OOM kills
|
||||
- no unexpected pod restarts
|
||||
|
||||
You can also run the staged helper, which writes a formal acceptance pack automatically:
|
||||
|
||||
```powershell
|
||||
python scripts\track7_stage.py --base-url http://<gateway-host> --namespace mvp-cc --auth-mode bearer
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
- `.artifacts/track7/staged_<timestamp>/step_250_250`
|
||||
- `.artifacts/track7/staged_<timestamp>/target_500_500`
|
||||
- `.artifacts/track7/staged_<timestamp>/acceptance_summary.json`
|
||||
- `.artifacts/track7/staged_<timestamp>/acceptance_summary.md`
|
||||
|
||||
## Target validation (`500 + 500`)
|
||||
|
||||
Only after the step profile passes:
|
||||
|
||||
```powershell
|
||||
python scripts\load_test.py --base-url http://<gateway-host> --profile target_500_500 --auth-mode bearer
|
||||
python scripts\track7_check.py --namespace mvp-cc --report-dir .artifacts\track7\<timestamp> --require-success-rate 99 --require-p95-seconds 2.0 --require-p99-seconds 3.5
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `500 voice + 500 digital` mixed profile
|
||||
- success rate `>= 99%`
|
||||
- p95 `<= 2.0s`
|
||||
- p99 `<= 3.5s`
|
||||
- `5xx` / transport failure rate `<= 0.5%`
|
||||
|
||||
## Evidence pack
|
||||
|
||||
Keep the following together:
|
||||
|
||||
- rendered Helm values/profile
|
||||
- `summary.json`
|
||||
- `latency_samples.csv`
|
||||
- `error_samples.json`
|
||||
- `mix_profile.json`
|
||||
- output from `track7_check.py`
|
||||
- rollout notes (cluster, namespace, date, profile, result)
|
||||
|
||||
## Rollback
|
||||
|
||||
If the target profile fails:
|
||||
|
||||
1. Roll back the Helm release:
|
||||
|
||||
```powershell
|
||||
helm rollback mvp-cc
|
||||
```
|
||||
|
||||
2. Keep the failed report directory for analysis.
|
||||
3. Reduce to the last passing profile and re-run validation only after tuning.
|
||||
|
||||
## Canonical scale500 values
|
||||
|
||||
Use the built-in preset as the base overlay for the production-like scale profile:
|
||||
|
||||
```powershell
|
||||
helm upgrade --install mvp-cc deployment\helm -f deployment\helm\values.scale500.yaml -n mvp-cc --create-namespace
|
||||
```
|
||||
|
||||
Then override at least:
|
||||
|
||||
- `global.databaseUrl`
|
||||
- image tag / repository as needed for the target cluster
|
||||
@@ -1,190 +0,0 @@
|
||||
# Wave 2 / Track 9.2 - Production Cutover (K8s Helm, Controlled Window)
|
||||
|
||||
This runbook executes Track 9.2 as an operations cutover, not a feature track.
|
||||
|
||||
## Goal
|
||||
|
||||
Move the accepted Asterisk bridge flow from QA to production Helm deployment with:
|
||||
|
||||
- strict bridge auth (`bearer` only),
|
||||
- one real softphone call validation,
|
||||
- recording import + supervisor playback proof,
|
||||
- formal GO/NO-GO evidence and rollback path.
|
||||
|
||||
## Scope
|
||||
|
||||
Includes:
|
||||
|
||||
- production Helm upgrade in strict mode,
|
||||
- post-cutover checks:
|
||||
- `track9_preflight --check-sftp --require-strict-service-auth`
|
||||
- `asterisk_lab_smoke --require-recording`
|
||||
- `track9_check --require-recording`
|
||||
- evidence package collection.
|
||||
|
||||
Excludes:
|
||||
|
||||
- ARI/call-control,
|
||||
- new voice features,
|
||||
- business API contract changes.
|
||||
|
||||
## Required inputs
|
||||
|
||||
Fill [track9-cutover-sheet.template.md](/e:/Zhan/docs/acceptance/track9/track9-cutover-sheet.template.md) first.
|
||||
|
||||
Mandatory values:
|
||||
|
||||
- `KUBE_CONTEXT`
|
||||
- `NAMESPACE`
|
||||
- `RELEASE`
|
||||
- `GATEWAY_BASE_URL`
|
||||
- `DATABASE_URL` (PostgreSQL in production)
|
||||
- `IMAGE_TAG`
|
||||
- `ASTERISK_AMI_HOST`, `ASTERISK_AMI_USERNAME`, `ASTERISK_AMI_SECRET`
|
||||
- `ASTERISK_SFTP_HOST`, `ASTERISK_SFTP_USERNAME`, `ASTERISK_SFTP_PASSWORD`
|
||||
- `QUEUE_ID` for `voice_lab`
|
||||
- `APP_TOKEN_SECRET`
|
||||
|
||||
## Phase 0 - Entry checks (T-1 day)
|
||||
|
||||
1. Baseline reference is fixed:
|
||||
- [docs/acceptance/track9/track9-acceptance.md](/e:/Zhan/docs/acceptance/track9/track9-acceptance.md)
|
||||
2. Confirm `voice_lab -> queue_id` exists in target routing DB.
|
||||
3. Confirm cluster can reach Asterisk VM:
|
||||
- AMI `5038/tcp`
|
||||
- SFTP `22/tcp`
|
||||
4. Confirm maintenance window and owners.
|
||||
|
||||
## Phase 1 - Pre-cutover dry-run (T-4h to T-1h)
|
||||
|
||||
Run dry-run command (no deploy):
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\track9_2_cutover.ps1 `
|
||||
-KubeContext <KUBE_CONTEXT> `
|
||||
-Namespace <NAMESPACE> `
|
||||
-Release <RELEASE> `
|
||||
-GatewayBaseUrl <GATEWAY_BASE_URL> `
|
||||
-DatabaseUrl <DATABASE_URL> `
|
||||
-ImageTag <IMAGE_TAG> `
|
||||
-AmiHost <ASTERISK_AMI_HOST> `
|
||||
-AmiUser <ASTERISK_AMI_USERNAME> `
|
||||
-AmiSecret <ASTERISK_AMI_SECRET> `
|
||||
-SftpHost <ASTERISK_SFTP_HOST> `
|
||||
-SftpUser <ASTERISK_SFTP_USERNAME> `
|
||||
-SftpPassword <ASTERISK_SFTP_PASSWORD> `
|
||||
-QueueId <QUEUE_ID> `
|
||||
-AppTokenSecret <APP_TOKEN_SECRET>
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `helm lint` PASS
|
||||
- `helm template` PASS
|
||||
- rendered manifest contains:
|
||||
- `ALLOW_LEGACY_HEADER_AUTH=0`
|
||||
- `ASTERISK_BRIDGE_AUTH_MODE=bearer`
|
||||
- `ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY=0`
|
||||
- `VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS` with `svc:asterisk-bridge`
|
||||
- `RECORDING_IMPORT_TRUSTED_SERVICE_SUBJECTS` with `svc:asterisk-bridge`
|
||||
- `RECORDING_IMPORT_ALLOW_ADMIN=0`
|
||||
|
||||
Artifacts are saved under:
|
||||
|
||||
- `.artifacts/track9_2/<timestamp>/`
|
||||
|
||||
## Phase 2 - Controlled cutover execute (T0)
|
||||
|
||||
Run same command with `-Execute`:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\track9_2_cutover.ps1 `
|
||||
-KubeContext <KUBE_CONTEXT> `
|
||||
-Namespace <NAMESPACE> `
|
||||
-Release <RELEASE> `
|
||||
-GatewayBaseUrl <GATEWAY_BASE_URL> `
|
||||
-DatabaseUrl <DATABASE_URL> `
|
||||
-ImageTag <IMAGE_TAG> `
|
||||
-AmiHost <ASTERISK_AMI_HOST> `
|
||||
-AmiUser <ASTERISK_AMI_USERNAME> `
|
||||
-AmiSecret <ASTERISK_AMI_SECRET> `
|
||||
-SftpHost <ASTERISK_SFTP_HOST> `
|
||||
-SftpUser <ASTERISK_SFTP_USERNAME> `
|
||||
-SftpPassword <ASTERISK_SFTP_PASSWORD> `
|
||||
-QueueId <QUEUE_ID> `
|
||||
-AppTokenSecret <APP_TOKEN_SECRET> `
|
||||
-Execute
|
||||
```
|
||||
|
||||
If Helm reports conflicts on HPA-managed `Deployment.spec.replicas`, rerun with:
|
||||
|
||||
```powershell
|
||||
... -Execute -ForceUpgrade
|
||||
```
|
||||
|
||||
Script actions:
|
||||
|
||||
1. snapshot:
|
||||
- `kubectl get pods -o wide`
|
||||
- `helm history` (before)
|
||||
2. `helm upgrade --install` with strict overlay
|
||||
3. wait for the migration Job to complete
|
||||
4. rollout wait:
|
||||
- `api-gateway`
|
||||
- `asterisk-bridge-service`
|
||||
- `voice-adapter-service`
|
||||
- `recording-service`
|
||||
5. strict preflight + smoke + track9_check
|
||||
6. evidence package via `track9_collect_evidence`
|
||||
7. writes rollback hint from Helm history
|
||||
|
||||
## Phase 3 - Live acceptance (mandatory)
|
||||
|
||||
During window, place one real call:
|
||||
|
||||
- softphone `1001 -> 7000`
|
||||
|
||||
Then confirm:
|
||||
|
||||
- `call.started` from Asterisk source exists
|
||||
- `call.ended` for same call exists
|
||||
- `recording.ready` uploaded to `recording-service`
|
||||
- supervisor playback is `HTTP 200` and `audio/*`
|
||||
|
||||
## Phase 4 - Evidence and GO/NO-GO
|
||||
|
||||
Use generated evidence folder from script and finalize:
|
||||
|
||||
- `track9-acceptance.md`
|
||||
- `playback-proof.md`
|
||||
- optional `playback-proof.json`
|
||||
|
||||
GO only if all true:
|
||||
|
||||
1. preflight PASS
|
||||
2. smoke PASS
|
||||
3. track9_check PASS
|
||||
4. `failed bridge events = 0`
|
||||
5. supervisor playback confirmed
|
||||
|
||||
## Rollback policy
|
||||
|
||||
Rollback triggers:
|
||||
|
||||
- preflight FAIL
|
||||
- track9_check FAIL
|
||||
- no recording import after live call
|
||||
- failed bridge events keep growing
|
||||
|
||||
Rollback command pattern:
|
||||
|
||||
```powershell
|
||||
helm -n <NAMESPACE> rollback <RELEASE> <PREVIOUS_REVISION>
|
||||
kubectl -n <NAMESPACE> rollout status deploy/api-gateway
|
||||
kubectl -n <NAMESPACE> rollout status deploy/asterisk-bridge-service
|
||||
```
|
||||
|
||||
After rollback:
|
||||
|
||||
- rerun `track9_preflight` in previous baseline,
|
||||
- log incident note + root cause item in cutover sheet.
|
||||
@@ -1,49 +0,0 @@
|
||||
# Voice Baseline
|
||||
|
||||
Status: `Track 11/12 accepted, Track 14 browser QA path added`
|
||||
|
||||
This document describes what the voice path already supports today.
|
||||
|
||||
## What works
|
||||
|
||||
- Asterisk 20 on a Linux VM forwards inbound lab calls into the platform through `asterisk-bridge-service`
|
||||
- A call from `1001` to `7000` creates a real voice interaction and writes:
|
||||
- `call.started`
|
||||
- `call.connected`
|
||||
- `call.ended`
|
||||
- `recording.ready`
|
||||
- `/operator` shows real bridge-backed live calls in `Активные звонки`
|
||||
- The operator can use accepted browser controls:
|
||||
- `Принять в работу`
|
||||
- `Завершить`
|
||||
- `Передать`
|
||||
- `/operator` keeps short-lived terminal cards in `Только что завершённые`
|
||||
- Recording import is automatic and `/supervisor` can play the audio back
|
||||
- A QA browser softphone path is now available when:
|
||||
- bridge WebRTC config is enabled
|
||||
- the operator user has a browser SIP mapping
|
||||
- Asterisk WSS config is applied
|
||||
|
||||
## What this baseline does not do
|
||||
|
||||
- no ARI
|
||||
- no attended transfer or conference
|
||||
- no production cutover workflow
|
||||
- no telephony redesign beyond the accepted Track 11 and Track 12 behavior
|
||||
- no production browser softphone rollout yet
|
||||
|
||||
## Source documents
|
||||
|
||||
- Track 11 roadmap: `docs/roadmap/07-track11-live-operator-call-control.md`
|
||||
- Track 12 roadmap: `docs/roadmap/08-track12-operator-voice-ux-hardening.md`
|
||||
- Track 14 roadmap: `docs/roadmap/09-track14-browser-softphone-webrtc.md`
|
||||
- Track 11 runbook: `docs/runbooks/track11-live-operator-call-control.md`
|
||||
- Track 12 runbook: `docs/runbooks/track12-operator-voice-ux.md`
|
||||
- Track 14 runbook: `docs/runbooks/track14-browser-softphone.md`
|
||||
|
||||
## Acceptance references
|
||||
|
||||
- Track 11 accepted package:
|
||||
- `docs/acceptance/track11/track11-acceptance.md`
|
||||
- Track 12 accepted package:
|
||||
- `docs/acceptance/track12/20260307_161629/track12-acceptance.md`
|
||||
@@ -1,24 +0,0 @@
|
||||
# Security Checklist (Stage 4)
|
||||
|
||||
## Scope
|
||||
- MVP on-prem deployment baseline
|
||||
- API gateway + service-level RBAC
|
||||
- Data persistence and backup handling
|
||||
|
||||
## Checklist
|
||||
- [x] RBAC enforced on privileged endpoints (`auth/users`, `routing/queues`, `knowledge/categories`)
|
||||
- [x] Role-denied requests return `403` without side effects
|
||||
- [x] Request payload validation enabled via Pydantic models
|
||||
- [x] SQL access uses parameterized ORM operations (SQLAlchemy)
|
||||
- [x] Health and registry endpoints do not expose secrets
|
||||
- [x] Backup archives created and restored via controlled scripts
|
||||
- [x] Security smoke checks automated in `scripts/gate4_check.py`
|
||||
|
||||
## Verification Commands
|
||||
- `python scripts/gate4_check.py`
|
||||
- Optional artifact retention: `python scripts/gate4_check.py --keep-artifacts`
|
||||
|
||||
## Out of Scope for MVP
|
||||
- LDAP/SSO integration
|
||||
- External secrets manager integration
|
||||
- WAF and SIEM integration
|
||||
@@ -1,59 +0,0 @@
|
||||
# UAT Pilot Package
|
||||
|
||||
This folder contains the operational UAT package for Stage 4 pilot acceptance.
|
||||
|
||||
## Scope Control
|
||||
|
||||
- The frozen MVP pilot scope is defined in `docs/gates/mvp-pilot-baseline.md`.
|
||||
- Out-of-scope requests must be redirected to `docs/roadmap/05-wave2-backlog.md`.
|
||||
- During pilot close-out, only `P1` and `P2` defects are remediated in MVP.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Run preflight before participants join:
|
||||
- Existing environment: `python scripts/uat_preflight.py --base-url http://<gateway-host>:8080`
|
||||
- Local auto-start: `python scripts/uat_preflight.py --auto-start`
|
||||
2. Run automated dry-run and generate UAT package:
|
||||
- Existing environment: `python scripts/uat_dry_run.py --base-url http://<gateway-host>:8080 --update-defect-register`
|
||||
- Local auto-start: `python scripts/uat_dry_run.py --auto-start --update-defect-register`
|
||||
3. Create/adjust a session protocol from template (or from generated dry-run package):
|
||||
- `docs/uat/session-template.md`
|
||||
- Or prepare a full manual bundle:
|
||||
- `python scripts/uat_manual_prepare.py --environment-url http://<pilot-gateway>:8080`
|
||||
4. Execute all scenarios:
|
||||
- `docs/uat/scenario-checklist.md`
|
||||
5. Register all defects:
|
||||
- `docs/uat/defect-log-template.csv`
|
||||
6. Capture sign-off:
|
||||
- `docs/uat/signoff-template.md`
|
||||
7. Attach preflight report from:
|
||||
- `docs/uat/evidence/`
|
||||
8. After signatures and zero open `P1/P2`, close Gate 4:
|
||||
- Optional validation only:
|
||||
- `python scripts/finalize_mvp_pilot.py --session-dir docs/uat/evidence/manual_<session-id> --dry-run`
|
||||
- `python scripts/finalize_mvp_pilot.py --session-dir docs/uat/evidence/manual_<session-id>`
|
||||
|
||||
## Triage Rules
|
||||
|
||||
- `P1`: pilot acceptance is impossible
|
||||
- `P2`: key MVP user journey is broken
|
||||
- `P3`: acceptable with limitation, defer unless it blocks sign-off
|
||||
- `P4`: cosmetic or low-priority backlog item
|
||||
|
||||
`P3` and `P4` findings should be logged and moved to post-MVP backlog instead of
|
||||
stretching the MVP remediation cycle.
|
||||
|
||||
## Mandatory Evidence Bundle
|
||||
|
||||
- preflight report
|
||||
- completed session protocol
|
||||
- completed scenario checklist
|
||||
- filled defect log
|
||||
- signed sign-off sheet
|
||||
- updated `docs/gates/p1-p2-defects.md`
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
- All mandatory scenarios executed
|
||||
- No open P1/P2 defects
|
||||
- Signed protocol by business and IT owners
|
||||
@@ -1,2 +0,0 @@
|
||||
defect_id,severity,status,scenario,service,risk_level,summary,repro_steps,actual_result,expected_result,verification_step,owner,opened_at,closed_at,backlog_bucket,notes
|
||||
UAT-001,P3,Open,S4,routing-service,Medium,"Example defect summary","1) ... 2) ...","Actual behavior","Expected behavior","Re-run scenario S4 after fix",owner_name,2026-02-26T00:00:00Z,,Wave2,
|
||||
|
@@ -1,3 +0,0 @@
|
||||
dry_run_*/
|
||||
manual_*/
|
||||
preflight_*.md
|
||||
@@ -1,8 +0,0 @@
|
||||
# UAT Evidence
|
||||
|
||||
Store generated pilot artifacts in this folder, for example:
|
||||
|
||||
- Preflight reports from `python scripts/uat_preflight.py`
|
||||
- Session protocol copies
|
||||
- Signed acceptance documents
|
||||
- Exported logs/screenshots
|
||||
@@ -1,223 +0,0 @@
|
||||
# UAT Scenario Checklist
|
||||
|
||||
## S0 Login and RBAC deny
|
||||
|
||||
- Preconditions:
|
||||
- Test users are available (`admin`, `supervisor`, `operator`, `analyst`)
|
||||
- Steps:
|
||||
- Sign in through the gateway
|
||||
- Verify role-dependent access to main flows
|
||||
- Attempt a denied privileged action as `operator` (for example create a user)
|
||||
- Expected:
|
||||
- Login succeeds
|
||||
- Allowed actions work for the selected role
|
||||
- Denied privileged action returns `403`
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- user:
|
||||
- screenshot/log:
|
||||
|
||||
## S1 Customer create and search
|
||||
|
||||
- Preconditions:
|
||||
- Operator UI or API access is available
|
||||
- Steps:
|
||||
- Create a customer
|
||||
- Search the customer by name or phone
|
||||
- Open the customer card
|
||||
- Expected:
|
||||
- Customer is created once
|
||||
- Search returns the same customer
|
||||
- Customer card data is consistent
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- customer_id:
|
||||
- screenshot/log:
|
||||
|
||||
## S2 Voice interaction lifecycle
|
||||
|
||||
- Preconditions:
|
||||
- Operator account is available
|
||||
- Voice queue is available
|
||||
- Steps:
|
||||
- Create a voice interaction
|
||||
- Assign interaction to operator
|
||||
- Change status to `closed`
|
||||
- Open the interaction timeline
|
||||
- Expected:
|
||||
- Status transitions are correct
|
||||
- Timeline contains creation, assignment, and close events
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- interaction_id:
|
||||
- screenshot/log:
|
||||
|
||||
## S3 Assignment and escalation to second line
|
||||
|
||||
- Preconditions:
|
||||
- Queue routing rule for escalation exists
|
||||
- Steps:
|
||||
- Create or select an active interaction
|
||||
- Assign it to an operator
|
||||
- Escalate it to the target queue
|
||||
- Expected:
|
||||
- Assignment is accepted
|
||||
- Queue is changed to the target queue
|
||||
- Status changes to `escalated`
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- interaction_id:
|
||||
- target_queue:
|
||||
|
||||
## S4 Routing and timeline verification
|
||||
|
||||
- Preconditions:
|
||||
- Queue rules are configured
|
||||
- At least one completed interaction exists
|
||||
- Steps:
|
||||
- Execute a route request for the active queue
|
||||
- Verify assigned operator and SLA result
|
||||
- Re-open interaction timeline
|
||||
- Expected:
|
||||
- Routing returns assignee and SLA
|
||||
- Timeline remains complete and chronological
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- queue_id:
|
||||
- route output:
|
||||
|
||||
## S5 Voice event intake
|
||||
|
||||
- Preconditions:
|
||||
- Voice adapter endpoint is reachable
|
||||
- Steps:
|
||||
- Submit a voice event (`call.started`, `ivr.completed`, or `call.ended`)
|
||||
- Verify event is stored and retrievable
|
||||
- Expected:
|
||||
- Voice event is accepted
|
||||
- Event ID is returned
|
||||
- Event can be listed afterwards
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- event_id:
|
||||
- call_id:
|
||||
|
||||
## S6 Telegram interaction lifecycle
|
||||
|
||||
- Preconditions:
|
||||
- Telegram adapter endpoint is reachable
|
||||
- Steps:
|
||||
- Send webhook payload to Telegram adapter
|
||||
- Verify message registration
|
||||
- Verify interaction visibility in operator workflow
|
||||
- Expected:
|
||||
- Message is accepted
|
||||
- Related interaction or adapter state is traceable
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- message_id:
|
||||
- screenshot/log:
|
||||
|
||||
## S7 KB usage in active handling
|
||||
|
||||
- Preconditions:
|
||||
- KB category/article created by analyst/supervisor
|
||||
- Steps:
|
||||
- Search KB by keyword
|
||||
- Open article
|
||||
- Use article during interaction processing
|
||||
- Expected:
|
||||
- Relevant article found
|
||||
- Operator confirms article usability
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- article_id:
|
||||
- keyword:
|
||||
|
||||
## S8 Supervisor realtime
|
||||
|
||||
- Preconditions:
|
||||
- At least two agent states are submitted
|
||||
- Steps:
|
||||
- Set agent states (`READY`, `BUSY`)
|
||||
- Update queue metrics
|
||||
- Open realtime view/API
|
||||
- Expected:
|
||||
- Agent totals and states are visible
|
||||
- Queue snapshot is visible
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- timestamp:
|
||||
- screenshot/log:
|
||||
|
||||
## S9 KPI report and export validation
|
||||
|
||||
- Preconditions:
|
||||
- KPI events are ingested for the test queue
|
||||
- Steps:
|
||||
- Open `/reports/kpi`
|
||||
- Verify KPI payload
|
||||
- Open `/reports/export` and capture CSV output
|
||||
- Expected:
|
||||
- KPI payload contains `SL`, `ASA`, `AHT`, `Abandon`, `FCR`
|
||||
- Export contains the expected header and queue rows
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- queue_id:
|
||||
- export snippet:
|
||||
|
||||
## O1 Backup and restore verification
|
||||
|
||||
- Preconditions:
|
||||
- Pilot environment contains test data
|
||||
- Steps:
|
||||
- Run backup script
|
||||
- Restore into a clean target
|
||||
- Verify the restored data set
|
||||
- Expected:
|
||||
- Archive is created
|
||||
- Restore succeeds
|
||||
- Core interaction data remains intact
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- archive:
|
||||
- restore check:
|
||||
|
||||
## O2 Load test baseline (`100 voice + 100 digital`)
|
||||
|
||||
- Preconditions:
|
||||
- Pilot environment is stable
|
||||
- Steps:
|
||||
- Run `python scripts/load_test.py --base-url http://<gateway-host>:8080 --voice 100 --digital 100`
|
||||
- Record success and latency output
|
||||
- Expected:
|
||||
- Load completes without blocking errors
|
||||
- MVP baseline target is met
|
||||
- Result:
|
||||
- [ ] Pass
|
||||
- [ ] Fail
|
||||
- Evidence:
|
||||
- command:
|
||||
- summary:
|
||||
@@ -1,72 +0,0 @@
|
||||
# UAT Session Protocol
|
||||
|
||||
## Session Metadata
|
||||
|
||||
- Session ID:
|
||||
- Date:
|
||||
- Start time:
|
||||
- End time:
|
||||
- Environment URL:
|
||||
- Build/version:
|
||||
- Deployment date:
|
||||
- Cluster/namespace:
|
||||
|
||||
## Scope Confirmation
|
||||
|
||||
- [ ] MVP pilot scope reviewed against `docs/gates/mvp-pilot-baseline.md`
|
||||
- [ ] Out-of-scope requests redirected to `docs/roadmap/05-wave2-backlog.md`
|
||||
- [ ] API stabilization rules acknowledged (backward-compatible fixes only)
|
||||
|
||||
## Participants
|
||||
|
||||
- Operators:
|
||||
- Supervisor:
|
||||
- Analyst:
|
||||
- Admin:
|
||||
- Business owner:
|
||||
- IT owner:
|
||||
|
||||
## Preconditions
|
||||
|
||||
- [ ] Preflight report attached (`docs/uat/evidence/preflight_*.md`)
|
||||
- [ ] Test accounts are active
|
||||
- [ ] Test queue is configured
|
||||
- [ ] Recording and audit logging are enabled
|
||||
- [ ] Defect log prepared from `docs/uat/defect-log-template.csv`
|
||||
|
||||
## Execution Summary
|
||||
|
||||
- Mandatory scenarios executed:
|
||||
- Passed:
|
||||
- Failed:
|
||||
- Blocked:
|
||||
|
||||
## Defect Summary
|
||||
|
||||
- P1:
|
||||
- P2:
|
||||
- P3:
|
||||
- P4:
|
||||
- Defect log attachment:
|
||||
|
||||
## Decision
|
||||
|
||||
- [ ] Accepted with conditions
|
||||
- [ ] Accepted for pilot completion
|
||||
- [ ] Not accepted
|
||||
|
||||
Accepted with conditions is allowed only when all `P1` and `P2` items are closed and
|
||||
remaining `P3`/`P4` findings are explicitly deferred to `Wave 2`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Business comments:
|
||||
- IT comments:
|
||||
- Deferred backlog references:
|
||||
|
||||
## Signatures
|
||||
|
||||
- Business owner:
|
||||
- IT owner:
|
||||
- Supervisor representative:
|
||||
- Date:
|
||||
@@ -1,48 +0,0 @@
|
||||
# UAT Sign-off Sheet
|
||||
|
||||
## Scope Confirmation
|
||||
|
||||
- MVP pilot scope confirmed against `docs/gates/mvp-pilot-baseline.md`
|
||||
- Out-of-scope items acknowledged and deferred to `docs/roadmap/05-wave2-backlog.md`
|
||||
- No new feature requests are included as MVP defects
|
||||
|
||||
## Acceptance Statement
|
||||
|
||||
The undersigned confirm that UAT scenarios were executed for MVP pilot scope and
|
||||
results are recorded in the session protocol.
|
||||
|
||||
## Conditions
|
||||
|
||||
- No open P1 defects
|
||||
- No open P2 defects
|
||||
- Mandatory scenarios completed
|
||||
|
||||
## Known Exclusions
|
||||
|
||||
- LDAP / SSO
|
||||
- additional channels beyond `voice` and `Telegram`
|
||||
- standalone supervisor/admin UI
|
||||
- advanced IVR builder
|
||||
- full recording suite
|
||||
- extended KPI catalog
|
||||
|
||||
## Decision
|
||||
|
||||
- [ ] Accepted for pilot completion
|
||||
- [ ] Accepted with conditions (only `P3`/`P4` deferred)
|
||||
- [ ] Not accepted
|
||||
|
||||
## Signatures
|
||||
|
||||
- Business owner:
|
||||
- Name:
|
||||
- Signature:
|
||||
- Date:
|
||||
- IT owner:
|
||||
- Name:
|
||||
- Signature:
|
||||
- Date:
|
||||
- Supervisor representative:
|
||||
- Name:
|
||||
- Signature:
|
||||
- Date:
|
||||
@@ -1,107 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from scripts import load_test, track7_check
|
||||
|
||||
|
||||
def test_load_test_profiles_and_mix():
|
||||
profile = load_test.resolve_profile("target_500_500")
|
||||
compat_profile = load_test.resolve_profile("baseline_100_100", voice=120, digital=80)
|
||||
mix = load_test.build_mix_profile(profile, include_read_traffic=True)
|
||||
|
||||
assert profile["voice"] == 500
|
||||
assert profile["digital"] == 500
|
||||
assert compat_profile["voice"] == 120
|
||||
assert compat_profile["digital"] == 80
|
||||
assert mix["channels"]["voice"] == 500
|
||||
assert mix["channels"]["telegram"] == 200
|
||||
assert mix["channels"]["webchat"] == 150
|
||||
assert mix["channels"]["email"] == 150
|
||||
assert "/proxy/reporting/reports/kpi" in mix["read_endpoints"]
|
||||
|
||||
|
||||
def test_load_test_request_builders_use_real_adapter_routes():
|
||||
telegram_request = load_test.build_request("telegram", 1)
|
||||
webchat_request = load_test.build_request("webchat", 1)
|
||||
email_request = load_test.build_request("email", 1)
|
||||
|
||||
assert telegram_request[1] == "/proxy/telegram/integrations/telegram/webhook"
|
||||
assert webchat_request[1] == "/proxy/webchat/integrations/webchat/messages"
|
||||
assert email_request[1] == "/proxy/email/integrations/email/messages"
|
||||
|
||||
|
||||
def test_load_test_report_writing_and_threshold_evaluation(tmp_path: Path):
|
||||
mix = load_test.build_mix_profile(load_test.resolve_profile("baseline_100_100"), include_read_traffic=False)
|
||||
results = [
|
||||
{"ts": 1.0, "target": "voice", "status_code": 200, "latency_seconds": 0.5, "ok": True},
|
||||
{"ts": 2.0, "target": "telegram", "status_code": 200, "latency_seconds": 0.7, "ok": True},
|
||||
{"ts": 3.0, "target": "voice", "status_code": 502, "latency_seconds": 1.2, "ok": False},
|
||||
]
|
||||
summary = load_test.build_summary(
|
||||
results,
|
||||
mix,
|
||||
{
|
||||
"require_success_rate": 50.0,
|
||||
"require_p95_seconds": 2.0,
|
||||
"require_p99_seconds": 3.0,
|
||||
},
|
||||
"2026-03-02T10:00:00",
|
||||
"2026-03-02T10:01:00",
|
||||
)
|
||||
|
||||
load_test.write_report(tmp_path, summary, results, [{"error": "bad gateway"}])
|
||||
|
||||
assert summary["passed"] is True
|
||||
assert (tmp_path / "summary.json").exists()
|
||||
assert (tmp_path / "latency_samples.csv").exists()
|
||||
assert (tmp_path / "error_samples.json").exists()
|
||||
assert (tmp_path / "mix_profile.json").exists()
|
||||
|
||||
|
||||
def test_track7_check_evaluate_summary_flags_failures():
|
||||
summary = {
|
||||
"results": {
|
||||
"success_rate": 97.5,
|
||||
"p95_seconds": 2.5,
|
||||
"p99_seconds": 4.1,
|
||||
"five_xx_rate": 0.8,
|
||||
}
|
||||
}
|
||||
|
||||
issues = track7_check.evaluate_summary(
|
||||
summary,
|
||||
require_success_rate=99.0,
|
||||
require_p95_seconds=2.0,
|
||||
require_p99_seconds=3.5,
|
||||
)
|
||||
|
||||
assert any("Success rate" in issue for issue in issues)
|
||||
assert any("P95" in issue for issue in issues)
|
||||
assert any("P99" in issue for issue in issues)
|
||||
assert any("5xx" in issue for issue in issues)
|
||||
|
||||
|
||||
def test_track7_artifacts_present_in_templates_and_runtime():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
db_py = (root / "services" / "shared" / "db.py").read_text(encoding="utf-8")
|
||||
values_yaml = (root / "deployment" / "helm" / "values.yaml").read_text(encoding="utf-8")
|
||||
services_yaml = (root / "deployment" / "helm" / "templates" / "services.yaml").read_text(encoding="utf-8")
|
||||
migration_job_yaml = (root / "deployment" / "helm" / "templates" / "migration-job.yaml").read_text(encoding="utf-8")
|
||||
|
||||
assert "DB_POOL_SIZE" in db_py
|
||||
assert "DB_MAX_OVERFLOW" in db_py
|
||||
assert "profiles:" in values_yaml
|
||||
assert "scale500:" in values_yaml
|
||||
assert "databaseUrl:" in values_yaml
|
||||
assert "migrations:" in values_yaml
|
||||
assert "HorizontalPodAutoscaler" in services_yaml
|
||||
assert "PodDisruptionBudget" in services_yaml
|
||||
assert "startupProbe" in services_yaml
|
||||
assert "livenessProbe" in services_yaml
|
||||
assert "DATABASE_URL" in services_yaml
|
||||
assert "ai-orchestrator-service:" in values_yaml
|
||||
assert "ai-voice-runtime-service:" in values_yaml
|
||||
assert "AI_VOICE_RUNTIME_SERVICE_URL" in services_yaml
|
||||
assert "AI_VOICE_QUEUE_CONFIG_JSON" in services_yaml
|
||||
assert '"helm.sh/hook": pre-install,pre-upgrade' in migration_job_yaml
|
||||
assert "scripts/migrate_core_db.py" in migration_job_yaml
|
||||
assert "SCHEMA_MANAGEMENT_MODE" in migration_job_yaml
|
||||
@@ -1,49 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from scripts import track7_stage
|
||||
|
||||
|
||||
def test_track7_stage_resolve_profiles():
|
||||
assert track7_stage.resolve_profiles("step_250_250,target_500_500") == ["step_250_250", "target_500_500"]
|
||||
|
||||
|
||||
def test_track7_stage_rejects_unknown_profile():
|
||||
try:
|
||||
track7_stage.resolve_profiles("step_250_250,unknown")
|
||||
except ValueError as exc:
|
||||
assert "Unsupported staged profiles" in str(exc)
|
||||
else:
|
||||
raise AssertionError("Expected ValueError for unknown staged profile")
|
||||
|
||||
|
||||
def test_track7_stage_writes_acceptance_pack(tmp_path: Path):
|
||||
payload = {
|
||||
"namespace": "mvp-cc",
|
||||
"base_url": "http://example.test",
|
||||
"auth_mode": "bearer",
|
||||
"include_read_traffic": True,
|
||||
"started_at": "2026-03-02T10:00:00",
|
||||
"completed_at": "2026-03-02T10:05:00",
|
||||
"passed": False,
|
||||
"stages": [
|
||||
{
|
||||
"profile": "step_250_250",
|
||||
"report_dir": str(tmp_path / "step_250_250"),
|
||||
"passed": False,
|
||||
"issues": ["P95 too high"],
|
||||
"results": {
|
||||
"success_rate": 100.0,
|
||||
"p95_seconds": 2.5,
|
||||
"p99_seconds": 3.0,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
track7_stage.write_acceptance_pack(tmp_path, payload)
|
||||
|
||||
assert (tmp_path / "acceptance_summary.json").exists()
|
||||
markdown = (tmp_path / "acceptance_summary.md").read_text(encoding="utf-8")
|
||||
assert "Track 7 Staged Validation" in markdown
|
||||
assert "step_250_250" in markdown
|
||||
assert "P95 too high" in markdown
|
||||
@@ -1,419 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import gateway.app as gateway_module
|
||||
from scripts import event_bus_smoke, track8_check
|
||||
from services.audit_service import app as audit_module
|
||||
from services.event_bus_service.app import app as event_bus_app
|
||||
from services.interaction_service.app import app as interaction_app
|
||||
from services.ivr_service.app import app as ivr_app
|
||||
from services.recording_service.app import app as recording_app
|
||||
from services.reporting_service import app as reporting_module
|
||||
from services.shared.event_bus import append_outbox_event, build_envelope, mark_outbox_failed
|
||||
from services.shared.sql_models import (
|
||||
AuditEventRow,
|
||||
Base,
|
||||
EventInboxRow,
|
||||
EventOutboxRow,
|
||||
ReportingEventLogRow,
|
||||
ReportingEventRow,
|
||||
)
|
||||
from services.supervisor_service.app import app as supervisor_app
|
||||
|
||||
|
||||
def _admin_headers() -> dict[str, str]:
|
||||
return {"X-User": "admin", "X-Role": "admin"}
|
||||
|
||||
|
||||
def _supervisor_headers() -> dict[str, str]:
|
||||
return {"X-User": "supervisor", "X-Role": "supervisor"}
|
||||
|
||||
|
||||
def _operator_headers() -> dict[str, str]:
|
||||
return {"X-User": "operator", "X-Role": "operator"}
|
||||
|
||||
|
||||
def _write_audio_fixture(path: Path) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"RIFF" + b"\x00" * 32)
|
||||
return path
|
||||
|
||||
|
||||
def _build_valid_flow(queue_id: str, resolved_queue_id: str) -> dict:
|
||||
return {
|
||||
"name": f"Track8 Flow {queue_id}",
|
||||
"description": "Track8 test flow",
|
||||
"queue_id": queue_id,
|
||||
"entry_node_id": "root",
|
||||
"flow_json": {
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": "root",
|
||||
"prompt_text": "Press 1 for support",
|
||||
"prompt_audio_key": "ivr/track8-root",
|
||||
"is_terminal": False,
|
||||
"invalid_target_node_id": "root",
|
||||
"options": [{"digit": "1", "target_node_id": "support"}],
|
||||
},
|
||||
{
|
||||
"node_id": "support",
|
||||
"prompt_text": "Routing to support",
|
||||
"prompt_audio_key": "ivr/track8-support",
|
||||
"is_terminal": True,
|
||||
"outcome_code": "support_route",
|
||||
"resolved_queue_id": resolved_queue_id,
|
||||
"resolved_queue_code": "support",
|
||||
"options": [],
|
||||
},
|
||||
]
|
||||
},
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
|
||||
def test_append_outbox_event_persists_full_envelope():
|
||||
from services.shared.db import get_session
|
||||
|
||||
session = get_session()
|
||||
try:
|
||||
entity_id = f"int_{uuid4().hex[:12]}"
|
||||
row = append_outbox_event(
|
||||
session,
|
||||
event_type="interaction.created",
|
||||
producer_service="interaction-service",
|
||||
entity_type="interaction",
|
||||
entity_id=entity_id,
|
||||
payload={"interaction_id": entity_id, "queue_id": "q_track8"},
|
||||
correlation_id="corr_track8",
|
||||
)
|
||||
session.commit()
|
||||
stored = session.execute(
|
||||
select(EventOutboxRow).where(EventOutboxRow.event_id == row.event_id)
|
||||
).scalar_one()
|
||||
envelope = json.loads(stored.payload_json)
|
||||
assert stored.status == "pending"
|
||||
assert stored.routing_key == "interaction.created"
|
||||
assert envelope["event_id"] == stored.event_id
|
||||
assert envelope["correlation_id"] == "corr_track8"
|
||||
assert envelope["payload"]["interaction_id"] == entity_id
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_interaction_supervisor_recording_and_ivr_write_outbox(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("EVENT_BUS_ENABLED", "1")
|
||||
interaction_client = TestClient(interaction_app)
|
||||
supervisor_client = TestClient(supervisor_app)
|
||||
recording_client = TestClient(recording_app)
|
||||
ivr_client = TestClient(ivr_app)
|
||||
|
||||
interaction = interaction_client.post(
|
||||
"/interactions",
|
||||
json={
|
||||
"channel": "voice",
|
||||
"subject": "Track8 Outbox Interaction",
|
||||
"queue_id": f"q_{uuid4().hex[:8]}",
|
||||
"priority": 3,
|
||||
},
|
||||
headers={"X-User": "operator", "X-Role": "operator"},
|
||||
)
|
||||
assert interaction.status_code == 200
|
||||
interaction_id = interaction.json()["interaction_id"]
|
||||
|
||||
assigned = interaction_client.patch(
|
||||
f"/interactions/{interaction_id}/assign",
|
||||
json={"assignee": "operator_a"},
|
||||
headers=_admin_headers(),
|
||||
)
|
||||
assert assigned.status_code == 200
|
||||
|
||||
escalated = interaction_client.post(
|
||||
f"/interactions/{interaction_id}/escalate",
|
||||
json={"target_queue_id": "line2"},
|
||||
headers=_supervisor_headers(),
|
||||
)
|
||||
assert escalated.status_code == 200
|
||||
|
||||
closed = interaction_client.patch(
|
||||
f"/interactions/{interaction_id}/status",
|
||||
json={"status": "closed"},
|
||||
headers={"X-User": "operator", "X-Role": "operator"},
|
||||
)
|
||||
assert closed.status_code == 200
|
||||
|
||||
agent_state = supervisor_client.post(
|
||||
"/supervisor/agent-states",
|
||||
json={"agent_id": f"agent_{uuid4().hex[:6]}", "state": "READY", "queue_id": "line2"},
|
||||
headers=_supervisor_headers(),
|
||||
)
|
||||
assert agent_state.status_code == 200
|
||||
|
||||
monkeypatch.setenv("CC_RECORDINGS_DIR", str(tmp_path / "recordings"))
|
||||
source_path = _write_audio_fixture(tmp_path / "fixtures" / "track8.wav")
|
||||
registered = recording_client.post(
|
||||
"/recordings/register",
|
||||
headers=_supervisor_headers(),
|
||||
json={
|
||||
"call_id": f"call_{uuid4().hex[:8]}",
|
||||
"interaction_id": interaction_id,
|
||||
"source_path": str(source_path),
|
||||
"file_name": "track8.wav",
|
||||
"mime_type": "audio/wav",
|
||||
"duration_seconds": 2,
|
||||
},
|
||||
)
|
||||
assert registered.status_code == 200
|
||||
|
||||
queue_id = f"ivr_q_{uuid4().hex[:6]}"
|
||||
flow = ivr_client.post("/ivr/flows", headers=_admin_headers(), json=_build_valid_flow(queue_id, "support_line"))
|
||||
assert flow.status_code == 200
|
||||
started = ivr_client.post(
|
||||
"/ivr/sessions/start",
|
||||
headers=_supervisor_headers(),
|
||||
json={"call_id": f"call_{uuid4().hex[:6]}", "queue_id": queue_id, "interaction_id": interaction_id},
|
||||
)
|
||||
assert started.status_code == 200
|
||||
session_id = started.json()["session"]["session_id"]
|
||||
completed = ivr_client.post(
|
||||
f"/ivr/sessions/{session_id}/dtmf",
|
||||
headers=_supervisor_headers(),
|
||||
json={"digit": "1"},
|
||||
)
|
||||
assert completed.status_code == 200
|
||||
assert completed.json()["completed"] is True
|
||||
|
||||
from services.shared.db import get_session
|
||||
|
||||
session = get_session()
|
||||
try:
|
||||
event_types = {
|
||||
row.event_type
|
||||
for row in session.execute(
|
||||
select(EventOutboxRow).where(
|
||||
EventOutboxRow.event_type.in_(
|
||||
[
|
||||
"interaction.created",
|
||||
"interaction.assigned",
|
||||
"interaction.escalated",
|
||||
"interaction.closed",
|
||||
"agent.state.changed",
|
||||
"call.recording.ready",
|
||||
"ivr.completed",
|
||||
]
|
||||
)
|
||||
)
|
||||
).scalars()
|
||||
}
|
||||
assert "interaction.created" in event_types
|
||||
assert "interaction.assigned" in event_types
|
||||
assert "interaction.escalated" in event_types
|
||||
assert "interaction.closed" in event_types
|
||||
assert "agent.state.changed" in event_types
|
||||
assert "call.recording.ready" in event_types
|
||||
assert "ivr.completed" in event_types
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_audit_and_reporting_consumers_are_idempotent() -> None:
|
||||
envelope = build_envelope(
|
||||
event_type="ivr.completed",
|
||||
producer="ivr-service",
|
||||
entity_type="ivr_session",
|
||||
entity_id=f"ivs_{uuid4().hex[:12]}",
|
||||
payload={
|
||||
"interaction_id": f"int_{uuid4().hex[:12]}",
|
||||
"queue_id": "line2",
|
||||
"resolved_queue_id": "support_line",
|
||||
"channel": "voice",
|
||||
"outcome_code": "support_route",
|
||||
},
|
||||
).model_dump()
|
||||
|
||||
audit_module._handle_event(envelope)
|
||||
audit_module._handle_event(envelope)
|
||||
reporting_module._handle_event(envelope)
|
||||
reporting_module._handle_event(envelope)
|
||||
|
||||
from services.shared.db import get_session
|
||||
|
||||
session = get_session()
|
||||
try:
|
||||
audit_rows = session.execute(
|
||||
select(AuditEventRow).where(AuditEventRow.action == "ivr.completed")
|
||||
).scalars().all()
|
||||
audit_inbox = session.execute(
|
||||
select(EventInboxRow).where(
|
||||
EventInboxRow.consumer_name == "audit-service",
|
||||
EventInboxRow.event_id == envelope["event_id"],
|
||||
)
|
||||
).scalars().all()
|
||||
reporting_log = session.execute(
|
||||
select(ReportingEventLogRow).where(ReportingEventLogRow.event_id == envelope["event_id"])
|
||||
).scalars().all()
|
||||
reporting_events = session.execute(
|
||||
select(ReportingEventRow).order_by(ReportingEventRow.id.desc())
|
||||
).scalars().all()
|
||||
reporting_inbox = session.execute(
|
||||
select(EventInboxRow).where(
|
||||
EventInboxRow.consumer_name == "reporting-service",
|
||||
EventInboxRow.event_id == envelope["event_id"],
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
assert len(audit_inbox) == 1
|
||||
assert len(reporting_inbox) == 1
|
||||
assert len([row for row in audit_rows if envelope["event_id"] in row.metadata_json]) == 1
|
||||
assert len(reporting_log) == 1
|
||||
assert any(row.queue_id == "support_line" for row in reporting_events)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_event_bus_service_retry_endpoint_and_gateway_contracts(monkeypatch) -> None:
|
||||
monkeypatch.setenv("EVENT_BUS_ENABLED", "0")
|
||||
from services.shared.db import get_session
|
||||
|
||||
session = get_session()
|
||||
try:
|
||||
row = append_outbox_event(
|
||||
session,
|
||||
event_type="interaction.created",
|
||||
producer_service="interaction-service",
|
||||
entity_type="interaction",
|
||||
entity_id=f"int_{uuid4().hex[:12]}",
|
||||
payload={"interaction_id": f"int_{uuid4().hex[:12]}"},
|
||||
)
|
||||
mark_outbox_failed(session, row, "broker down")
|
||||
event_id = row.event_id
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
client = TestClient(event_bus_app)
|
||||
retried = client.post(f"/bus/outbox/{event_id}/retry", headers=_admin_headers())
|
||||
assert retried.status_code == 200
|
||||
assert retried.json()["status"] == "pending"
|
||||
|
||||
denied = client.get("/bus/outbox", headers=_operator_headers())
|
||||
assert denied.status_code == 403
|
||||
|
||||
gateway_client = TestClient(gateway_module.app)
|
||||
contracts = gateway_client.get("/contracts")
|
||||
assert contracts.status_code == 200
|
||||
body = contracts.json()
|
||||
assert "stage_8" in body
|
||||
assert "/bus/outbox" in body["stage_8"]
|
||||
|
||||
|
||||
def test_event_bus_smoke_and_track8_check_with_temp_database(tmp_path: Path, monkeypatch) -> None:
|
||||
db_path = tmp_path / "track8.db"
|
||||
engine = create_engine(f"sqlite:///{db_path.as_posix()}", future=True)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, status_code: int, payload: dict | None = None):
|
||||
self.status_code = status_code
|
||||
self._payload = payload or {}
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
if self.status_code >= 400:
|
||||
raise RuntimeError("HTTP error")
|
||||
|
||||
def json(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
class DummyClient:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.base_url = kwargs.get("base_url")
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return None
|
||||
|
||||
def get(self, path: str, headers: dict | None = None, timeout: int | None = None):
|
||||
if path == "/proxy/event-bus/health":
|
||||
return DummyResponse(200, {"status": "ok"})
|
||||
raise AssertionError(f"Unexpected GET path {path}")
|
||||
|
||||
def post(self, path: str, headers: dict | None = None, json: dict | None = None, timeout: int | None = None):
|
||||
if path == "/proxy/interaction/interactions":
|
||||
interaction_id = f"int_{uuid4().hex[:12]}"
|
||||
with Session(engine) as session:
|
||||
outbox = EventOutboxRow(
|
||||
event_id=f"evt_{uuid4().hex[:12]}",
|
||||
event_type="interaction.created",
|
||||
event_version=1,
|
||||
producer_service="interaction-service",
|
||||
entity_type="interaction",
|
||||
entity_id=interaction_id,
|
||||
correlation_id=None,
|
||||
routing_key="interaction.created",
|
||||
payload_json=json and __import__("json").dumps(
|
||||
{
|
||||
"event_id": f"evt_payload_{uuid4().hex[:12]}",
|
||||
"event_type": "interaction.created",
|
||||
"event_version": 1,
|
||||
"occurred_at": "2026-03-02T12:00:00+00:00",
|
||||
"producer": "interaction-service",
|
||||
"entity_type": "interaction",
|
||||
"entity_id": interaction_id,
|
||||
"routing_key": "interaction.created",
|
||||
"payload": {"interaction_id": interaction_id},
|
||||
}
|
||||
)
|
||||
or "{}",
|
||||
status="published",
|
||||
attempt_count=0,
|
||||
last_error=None,
|
||||
available_at="2026-03-02T12:00:00+00:00",
|
||||
published_at="2026-03-02T12:00:01+00:00",
|
||||
created_at="2026-03-02T12:00:00+00:00",
|
||||
updated_at="2026-03-02T12:00:01+00:00",
|
||||
)
|
||||
session.add(outbox)
|
||||
session.add(
|
||||
EventInboxRow(
|
||||
consumer_name="audit-service",
|
||||
event_id=outbox.event_id,
|
||||
event_type=outbox.event_type,
|
||||
processed_at="2026-03-02T12:00:02+00:00",
|
||||
status="processed",
|
||||
notes=None,
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
EventInboxRow(
|
||||
consumer_name="reporting-service",
|
||||
event_id=outbox.event_id,
|
||||
event_type=outbox.event_type,
|
||||
processed_at="2026-03-02T12:00:02+00:00",
|
||||
status="processed",
|
||||
notes=None,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
return DummyResponse(200, {"interaction_id": interaction_id})
|
||||
raise AssertionError(f"Unexpected POST path {path}")
|
||||
|
||||
monkeypatch.setattr(event_bus_smoke, "_engine_for", lambda database_url=None: engine)
|
||||
monkeypatch.setattr(event_bus_smoke.httpx, "Client", DummyClient)
|
||||
|
||||
smoke = event_bus_smoke.run_smoke_check(base_url="http://example.local", auth_mode="legacy_headers")
|
||||
assert smoke["passed"] is True
|
||||
|
||||
issues = track8_check.evaluate_track8(
|
||||
database_url=f"sqlite:///{db_path.as_posix()}",
|
||||
require_bus_enabled=False,
|
||||
base_url=None,
|
||||
)
|
||||
assert issues == []
|
||||
Reference in New Issue
Block a user