docs: recreate documentation structure with updated specs

This commit is contained in:
Yera All
2026-04-06 23:19:24 +05:00
parent 1497bb0feb
commit 1ff8fd7102
69 changed files with 274 additions and 5427 deletions
-2
View File
@@ -1,2 +0,0 @@
APP_IMAGE=registry.example.com/group/project/app:latest
ASTERISK_IMAGE=registry.example.com/group/project/asterisk:latest
-91
View File
@@ -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
-59
View File
@@ -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
}
-30
View File
@@ -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
}
+31 -27
View File
@@ -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-схем, описанных выше
}
}
```
-54
View File
@@ -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
}
+50 -22
View File
@@ -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` логики обмена.
-744
View File
@@ -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.
+39
View File
@@ -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 (Сводка):** Что клиент сказал, что ИИ попытался сделать и почему перевел звонок на человека.
- **Таймлайн:** В историю обращений клиента логгируется, что "ИИ запрашивал перевод на специалиста".
-8
View File
@@ -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
-8
View File
@@ -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`
-35
View File
@@ -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`.
-71
View File
@@ -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
-16
View File
@@ -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.
-12
View File
@@ -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/`
-37
View File
@@ -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`)
-22
View File
@@ -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)
-25
View File
@@ -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
-21
View File
@@ -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
-22
View File
@@ -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
-126
View File
@@ -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
-30
View File
@@ -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
-11
View File
@@ -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
-132
View File
@@ -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.
-315
View File
@@ -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`
-20
View File
@@ -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
-79
View File
@@ -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`.
-175
View File
@@ -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`.
+60
View File
@@ -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`.
-83
View File
@@ -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.
-80
View File
@@ -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
```
-86
View File
@@ -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.
-102
View File
@@ -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
-128
View File
@@ -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.
-20
View File
@@ -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
-65
View File
@@ -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)
+94
View File
@@ -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
```
-39
View File
@@ -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.
-88
View File
@@ -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
-275
View File
@@ -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.
-88
View File
@@ -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
-65
View File
@@ -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 `Принять в работу`
-44
View File
@@ -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
-134
View File
@@ -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.
-49
View File
@@ -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`
-24
View File
@@ -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
-59
View File
@@ -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
-2
View File
@@ -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 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
2 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
-3
View File
@@ -1,3 +0,0 @@
dry_run_*/
manual_*/
preflight_*.md
-8
View File
@@ -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
-223
View File
@@ -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:
-72
View File
@@ -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:
-48
View File
@@ -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:
-107
View File
@@ -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
-49
View File
@@ -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
-419
View File
@@ -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 == []