Initial import with GitLab CI/CD and registry deploy flow

This commit is contained in:
Yera All
2026-04-02 17:05:45 +05:00
commit 5374c202d9
338 changed files with 81297 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
.git
.github
.pytest_cache
__pycache__/
*.py[cod]
.artifacts
.local_stack
.tmp
test-results
.data
.data_local
.data_smoke
.data_gate*
.data_uat_*
docs
tests
.env
.env.local
.env.production
backups
+132
View File
@@ -0,0 +1,132 @@
# Gateway
GATEWAY_PORT=8080
CC_DATA_DIR=.data
# Shared DB URL for services (sqlite by default; switch to postgres as needed)
DATABASE_URL=sqlite:///./.data/mvp_cc.db
SCHEMA_MANAGEMENT_MODE=legacy
# DATABASE_URL=postgresql://mvp:mvp@localhost:5432/mvpcc
# SCHEMA_MANAGEMENT_MODE=migrations
AUTH_SERVICE_URL=http://localhost:8001
AUDIT_SERVICE_URL=http://localhost:8002
CUSTOMER_SERVICE_URL=http://localhost:8003
INTERACTION_SERVICE_URL=http://localhost:8004
ROUTING_SERVICE_URL=http://localhost:8005
VOICE_ADAPTER_SERVICE_URL=http://localhost:8006
TELEGRAM_ADAPTER_SERVICE_URL=http://localhost:8007
KB_SERVICE_URL=http://localhost:8008
REPORTING_SERVICE_URL=http://localhost:8009
SUPERVISOR_SERVICE_URL=http://localhost:8010
TELEGRAM_BOT_ENABLED=0
TELEGRAM_BOT_TOKEN=
TELEGRAM_WEBHOOK_SECRET=
TELEGRAM_DEFAULT_QUEUE_ID=q_telegram
TELEGRAM_BOT_API_BASE=https://api.telegram.org
WHATSAPP_DEFAULT_QUEUE_ID=q_whatsapp
WHATSAPP_WEBHOOK_SECRET=
WHATSAPP_META_ACCESS_TOKEN=
WHATSAPP_META_PHONE_NUMBER_ID=
WHATSAPP_META_VERIFY_TOKEN=
WHATSAPP_META_APP_SECRET=
WHATSAPP_META_GRAPH_API_VERSION=v22.0
WHATSAPP_OUTBOUND_ENABLED=0
AI_PROVIDER=stub
AI_API_BASE=
AI_API_KEY=
AI_MODEL=stub-telegram-assistant
AI_TIMEOUT_SECONDS=20
AI_TELEGRAM_ENABLED=0
AI_TELEGRAM_ALWAYS_REPLY=0
AI_TELEGRAM_MAX_CONTEXT_MESSAGES=20
AI_TELEGRAM_MAX_KB_RESULTS=3
AI_TELEGRAM_CONFIDENCE_HANDOFF_THRESHOLD=0.65
AI_WHATSAPP_ENABLED=0
AI_WHATSAPP_ALWAYS_REPLY=0
AI_WHATSAPP_MAX_CONTEXT_MESSAGES=20
AI_WHATSAPP_MAX_KB_RESULTS=3
AI_WHATSAPP_CONFIDENCE_HANDOFF_THRESHOLD=0.65
AI_VOICE_RUNTIME_SERVICE_URL=http://localhost:8018
AI_VOICE_ENABLED=0
AI_VOICE_QUEUE_CONFIG_JSON={}
AI_VOICE_ASR_PROVIDER=openai
AI_VOICE_TTS_PROVIDER=openai
AI_VOICE_AUDIOSOCKET_ENABLED=0
AI_VOICE_AUDIOSOCKET_HOST=0.0.0.0
AI_VOICE_AUDIOSOCKET_PORT=9019
AI_VOICE_ASR_MODEL=gpt-4o-mini-transcribe
AI_VOICE_TTS_MODEL=gpt-4o-mini-tts
AI_VOICE_TTS_VOICE=alloy
AI_VOICE_TTS_YANDEX_API_BASE=https://tts.api.ml.yandexcloud.kz
AI_VOICE_TTS_YANDEX_API_KEY=
AI_VOICE_TTS_YANDEX_IAM_TOKEN=
AI_VOICE_TTS_YANDEX_FOLDER_ID=
AI_VOICE_TTS_YANDEX_VOICE=jane
AI_VOICE_TTS_YANDEX_KK_VOICE=amira
AI_VOICE_TTS_YANDEX_SPEED=1.0
AI_VOICE_TTS_YANDEX_ROLE=
AI_VOICE_TTS_YANDEX_EMOTION=
AI_VOICE_TTS_YANDEX_SAMPLE_RATE_HZ=8000
AI_VOICE_TTS_CACHE_ENABLED=1
AI_VOICE_TTS_CACHE_DIR=.data/ai_voice_tts_cache
AI_VOICE_VAD_MIN_SPEECH_MS=300
AI_VOICE_VAD_TRAILING_SILENCE_MS=700
AI_VOICE_TURN_MAX_MS=10000
AI_VOICE_MEDIA_IDLE_TIMEOUT_SECONDS=15
AI_VOICE_MAX_CONTEXT_SEGMENTS=8
AI_VOICE_HANDOFF_TIMEOUT_SECONDS=8
AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:ai-voice-runtime
# Event bus (Track 8)
EVENT_BUS_ENABLED=0
EVENT_BUS_URL=amqp://guest:guest@localhost:5672/
EVENT_BUS_EXCHANGE=mvpcc.domain.events
EVENT_BUS_CONSUMER_ENABLED=1
EVENT_BUS_AUDIT_QUEUE=mvpcc.audit.events
EVENT_BUS_REPORTING_QUEUE=mvpcc.reporting.events
# Asterisk bridge (Track 9)
ASTERISK_BRIDGE_ENABLED=0
ASTERISK_AMI_HOST=
ASTERISK_AMI_PORT=5038
ASTERISK_AMI_USERNAME=
ASTERISK_AMI_SECRET=
ASTERISK_AMI_EVENT_PREFIX=MVPCC
ASTERISK_QUEUE_MAP_JSON={}
ASTERISK_BRIDGE_POLL_INTERVAL_SECONDS=1
ASTERISK_IVR_FASTAGI_ENABLED=0
ASTERISK_IVR_FASTAGI_HOST=127.0.0.1
ASTERISK_IVR_FASTAGI_PORT=4573
ASTERISK_IVR_DTMF_TIMEOUT_SECONDS=5
ASTERISK_IVR_MAX_NO_INPUT_RETRIES=2
ASTERISK_IVR_MAX_INVALID_RETRIES=2
ASTERISK_IVR_CALL_LINK_WAIT_SECONDS=5
# bridge internal auth mode:
# - legacy_headers (default QA baseline)
# - bearer
# - bearer_first (try bearer, fallback to legacy on 401/403)
ASTERISK_BRIDGE_AUTH_MODE=legacy_headers
ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY=1
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_CALLCONTROL_ENABLED=0
ASTERISK_CALLCONTROL_ACTION_TIMEOUT_SECONDS=10
ASTERISK_WEBRTC_ENABLED=0
ASTERISK_WEBRTC_WS_URL=
ASTERISK_WEBRTC_ICE_SERVERS_JSON=[]
ASTERISK_OPERATOR_EXTENSION_MAP_JSON={}
ASTERISK_BROWSER_SIP_MAP_JSON={}
ASTERISK_TRANSFER_TARGET_MAP_JSON={}
ASTERISK_SFTP_HOST=
ASTERISK_SFTP_PORT=22
ASTERISK_SFTP_USERNAME=
ASTERISK_SFTP_PASSWORD=
ASTERISK_SFTP_BASE_PATH=/var/spool/asterisk/monitor
# Track 9.1 service-token hardening
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
+17
View File
@@ -0,0 +1,17 @@
# Local PostgreSQL profile for scripts/local_stack.py --env-file
CC_DATA_DIR=.data_local_pg
DATABASE_URL=postgresql://mvp:mvp@127.0.0.1:5432/mvpcc
SCHEMA_MANAGEMENT_MODE=migrations
APP_TOKEN_SECRET=dev-secret-change-me
ALLOW_LEGACY_HEADER_AUTH=1
EVENT_BUS_ENABLED=0
EVENT_BUS_URL=amqp://guest:guest@127.0.0.1:5672/
ASTERISK_BRIDGE_ENABLED=0
TELEGRAM_BOT_ENABLED=0
WHATSAPP_OUTBOUND_ENABLED=0
AI_PROVIDER=stub
AI_TELEGRAM_ENABLED=0
AI_WHATSAPP_ENABLED=0
AI_VOICE_ENABLED=0
+25
View File
@@ -0,0 +1,25 @@
# Server-only overrides for a parallel PostgreSQL-backed call-center stack.
# Keep .env.production as the base, then copy this file to .env.postgres.parallel.server
# and replace <server-host> plus the password placeholder.
CC_DATA_DIR=.data_pg_parallel
DATABASE_URL=postgresql://mvpcc_app:change-me-with-the-password-from-.env.postgres.server@call-center-postgres:5432/mvpcc
SCHEMA_MANAGEMENT_MODE=migrations
LOCAL_STACK_GATEWAY_PORT=18080
LOCAL_STACK_PUBLIC_BASE_URL=http://<server-host>:18080
# Keep the parallel stack passive to avoid duplicate side effects while SQLite remains live.
EVENT_BUS_ENABLED=0
TELEGRAM_BOT_ENABLED=0
AI_TELEGRAM_ENABLED=0
AI_TELEGRAM_ALWAYS_REPLY=0
WHATSAPP_OUTBOUND_ENABLED=0
AI_WHATSAPP_ENABLED=0
AI_WHATSAPP_ALWAYS_REPLY=0
AI_VOICE_ENABLED=0
AI_VOICE_AUDIOSOCKET_ENABLED=0
AI_VOICE_TTS_CACHE_DIR=/app/.data_pg_parallel/ai_voice_tts_cache
ASTERISK_BRIDGE_ENABLED=0
ASTERISK_IVR_FASTAGI_ENABLED=0
ASTERISK_CALLCONTROL_ENABLED=0
ASTERISK_WEBRTC_ENABLED=0
+6
View File
@@ -0,0 +1,6 @@
# Server-only template for deployment/docker-compose.postgres.server.yml
# Copy to .env.postgres.server on the target host and replace the password.
POSTGRES_DB=mvpcc
POSTGRES_USER=mvpcc_app
POSTGRES_PASSWORD=change-me-with-a-strong-random-secret
+128
View File
@@ -0,0 +1,128 @@
# Track 9 QA lab template (do not commit real secrets into .env.production)
# Core runtime
CC_DATA_DIR=.data
DATABASE_URL=postgresql://mvp:mvp@<postgres-host>:5432/mvpcc
SCHEMA_MANAGEMENT_MODE=migrations
# Internal service URLs (cluster/internal or host-routed URLs)
INTERACTION_SERVICE_URL=http://interaction-service:8000
VOICE_ADAPTER_SERVICE_URL=http://voice-adapter-service:8000
RECORDING_SERVICE_URL=http://recording-service:8000
TELEGRAM_ADAPTER_SERVICE_URL=http://telegram-adapter-service:8000
AI_ORCHESTRATOR_SERVICE_URL=http://ai-orchestrator-service:8000
AI_VOICE_RUNTIME_SERVICE_URL=http://ai-voice-runtime-service:8000
# Telegram real-bot chat (Track 15)
TELEGRAM_BOT_ENABLED=1
TELEGRAM_BOT_TOKEN=<telegram-bot-token>
TELEGRAM_WEBHOOK_SECRET=<telegram-webhook-secret>
TELEGRAM_DEFAULT_QUEUE_ID=<platform-telegram-queue-id>
TELEGRAM_BOT_API_BASE=https://api.telegram.org
WHATSAPP_DEFAULT_QUEUE_ID=<platform-whatsapp-queue-id-or-q_whatsapp>
WHATSAPP_WEBHOOK_SECRET=<legacy-whatsapp-webhook-secret-optional>
WHATSAPP_META_ACCESS_TOKEN=<meta-whatsapp-access-token>
WHATSAPP_META_PHONE_NUMBER_ID=<meta-whatsapp-phone-number-id>
WHATSAPP_META_VERIFY_TOKEN=<meta-whatsapp-verify-token>
WHATSAPP_META_APP_SECRET=<meta-app-secret>
WHATSAPP_META_GRAPH_API_VERSION=v22.0
WHATSAPP_OUTBOUND_ENABLED=0
AI_PROVIDER=openai_compatible
AI_API_BASE=https://<llm-host>/v1
AI_API_KEY=<llm-api-key>
AI_MODEL=<llm-model>
AI_TIMEOUT_SECONDS=20
AI_TELEGRAM_ENABLED=0
AI_TELEGRAM_ALWAYS_REPLY=0
AI_TELEGRAM_MAX_CONTEXT_MESSAGES=20
AI_TELEGRAM_MAX_KB_RESULTS=3
AI_TELEGRAM_CONFIDENCE_HANDOFF_THRESHOLD=0.65
AI_WHATSAPP_ENABLED=0
AI_WHATSAPP_ALWAYS_REPLY=0
AI_WHATSAPP_MAX_CONTEXT_MESSAGES=20
AI_WHATSAPP_MAX_KB_RESULTS=3
AI_WHATSAPP_CONFIDENCE_HANDOFF_THRESHOLD=0.65
AI_VOICE_ENABLED=0
AI_VOICE_QUEUE_CONFIG_JSON={"voice_lab_ai":{"mode":"ai_first","agent_profile":"voice_support","handoff_queue_code":"voice_lab","language":"ru"}}
AI_VOICE_ASR_PROVIDER=openai
AI_VOICE_TTS_PROVIDER=openai
AI_VOICE_AUDIOSOCKET_ENABLED=1
AI_VOICE_AUDIOSOCKET_HOST=0.0.0.0
AI_VOICE_AUDIOSOCKET_PORT=9019
AI_VOICE_ASR_MODEL=gpt-4o-mini-transcribe
AI_VOICE_TTS_MODEL=gpt-4o-mini-tts
AI_VOICE_TTS_VOICE=alloy
AI_VOICE_TTS_YANDEX_API_BASE=https://tts.api.ml.yandexcloud.kz
AI_VOICE_TTS_YANDEX_API_KEY=<yandex-api-key-optional>
AI_VOICE_TTS_YANDEX_IAM_TOKEN=<yandex-iam-token-optional>
AI_VOICE_TTS_YANDEX_FOLDER_ID=<yandex-folder-id-for-user-iam-token-optional>
AI_VOICE_TTS_YANDEX_VOICE=jane
AI_VOICE_TTS_YANDEX_KK_VOICE=amira
AI_VOICE_TTS_YANDEX_SPEED=1.0
AI_VOICE_TTS_YANDEX_ROLE=
# backward-compatible alias for old v1-style config
AI_VOICE_TTS_YANDEX_EMOTION=
AI_VOICE_TTS_YANDEX_SAMPLE_RATE_HZ=8000
AI_VOICE_TTS_CACHE_ENABLED=1
AI_VOICE_TTS_CACHE_DIR=/app/.data_local/ai_voice_tts_cache
AI_VOICE_VAD_MIN_SPEECH_MS=300
AI_VOICE_VAD_TRAILING_SILENCE_MS=700
AI_VOICE_TURN_MAX_MS=10000
AI_VOICE_MEDIA_IDLE_TIMEOUT_SECONDS=15
AI_VOICE_MAX_CONTEXT_SEGMENTS=8
AI_VOICE_HANDOFF_TIMEOUT_SECONDS=8
AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:ai-voice-runtime
# Event bus compatibility (Track 8 baseline)
EVENT_BUS_ENABLED=1
EVENT_BUS_URL=amqp://guest:guest@<rabbitmq-host>:5672/
EVENT_BUS_EXCHANGE=mvpcc.domain.events
EVENT_BUS_CONSUMER_ENABLED=1
# Asterisk bridge (Track 9)
ASTERISK_BRIDGE_ENABLED=1
ASTERISK_AMI_HOST=<asterisk-linux-vm-host>
ASTERISK_AMI_PORT=5038
ASTERISK_AMI_USERNAME=mvpcc
ASTERISK_AMI_SECRET=<replace-me>
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_POLL_INTERVAL_SECONDS=1
# Bridge internal auth for QA:
# keep legacy_headers for Track 9 acceptance baseline.
# use bearer_first or bearer in Track 9.1 hardening.
ASTERISK_BRIDGE_AUTH_MODE=legacy_headers
ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY=1
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
ASTERISK_IVR_FASTAGI_PORT=4573
ASTERISK_IVR_DTMF_TIMEOUT_SECONDS=5
ASTERISK_IVR_MAX_NO_INPUT_RETRIES=2
ASTERISK_IVR_MAX_INVALID_RETRIES=2
ASTERISK_IVR_CALL_LINK_WAIT_SECONDS=5
ASTERISK_CALLCONTROL_ENABLED=0
ASTERISK_CALLCONTROL_ACTION_TIMEOUT_SECONDS=10
ASTERISK_WEBRTC_ENABLED=0
ASTERISK_WEBRTC_WS_URL=wss://<asterisk-linux-vm-host>:8089/ws
ASTERISK_WEBRTC_ICE_SERVERS_JSON=[]
ASTERISK_OPERATOR_EXTENSION_MAP_JSON={"operator_a":"2001","operator_b":"2002"}
ASTERISK_BROWSER_SIP_MAP_JSON={"operator":{"authorization_username":"2001","password":"<browser-sip-password>","display_name":"Operator Browser","operator_extension":"2001"}}
ASTERISK_TRANSFER_TARGET_MAP_JSON={"voice_lab":"2001","voice_lab_ai":"2001","voice_lab_ivr":"7200"}
# Track 9.1 service-token hardening
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
# set to 0 in strict QA/prod mode
RECORDING_IMPORT_ALLOW_ADMIN=1
# SFTP pickup from Asterisk VM
ASTERISK_SFTP_HOST=<asterisk-linux-vm-host>
ASTERISK_SFTP_PORT=22
ASTERISK_SFTP_USERNAME=<linux-user>
ASTERISK_SFTP_PASSWORD=<replace-me>
ASTERISK_SFTP_BASE_PATH=/var/spool/asterisk/monitor
+91
View File
@@ -0,0 +1,91 @@
name: ci
on:
push:
branches: ["main", "master"]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytest -q
postgres-readiness:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: mvpcc
POSTGRES_USER: mvp
POSTGRES_PASSWORD: mvp
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U mvp -d mvpcc"
--health-interval 10s
--health-timeout 5s
--health-retries 10
rabbitmq:
image: rabbitmq:3-management
ports:
- 5672:5672
- 15672:15672
env:
DATABASE_URL: postgresql://mvp:mvp@localhost:5432/mvpcc
SCHEMA_MANAGEMENT_MODE: migrations
CC_DATA_DIR: .testdata_pg
EVENT_BUS_ENABLED: "0"
EVENT_BUS_URL: amqp://guest:guest@localhost:5672/
APP_TOKEN_SECRET: ci-secret
ALLOW_LEGACY_HEADER_AUTH: "1"
TELEGRAM_BOT_ENABLED: "0"
WHATSAPP_OUTBOUND_ENABLED: "0"
AI_PROVIDER: stub
AI_TELEGRAM_ENABLED: "0"
AI_WHATSAPP_ENABLED: "0"
AI_VOICE_ENABLED: "0"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Apply migrations
run: python scripts/migrate_core_db.py
- name: Run PostgreSQL readiness smoke
run: pytest -q tests/test_postgres_readiness.py
build-image:
runs-on: ubuntu-latest
needs: [test, postgres-readiness]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build image
run: docker build -t mvp-cc-platform:ci .
+22
View File
@@ -0,0 +1,22 @@
.data/
.data_local/
.data_pg/
.data_pg_parallel/
.data_smoke/
.data_gate*/
.data_uat_*/
.artifacts/
.local_stack/
.codex_tmp/
.server_backups/
.tmp/
test-results/
.testdata/
backups/
__pycache__/
**/__pycache__/
.pytest_cache/
.env.production
.env.postgres.server
.env.postgres.parallel.server
deployment/.env.images
+154
View File
@@ -0,0 +1,154 @@
stages:
- test
- build
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
APP_IMAGE_REPOSITORY: "$CI_REGISTRY_IMAGE/app"
ASTERISK_IMAGE_REPOSITORY: "$CI_REGISTRY_IMAGE/asterisk"
APP_IMAGE_TAG: "$CI_COMMIT_SHA"
ASTERISK_IMAGE_TAG: "$CI_COMMIT_SHA"
APP_IMAGE: "$CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA"
ASTERISK_IMAGE: "$CI_REGISTRY_IMAGE/asterisk:$CI_COMMIT_SHA"
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: "tcp://docker:2376"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
default:
cache:
paths:
- .cache/pip
.python_job:
image: python:3.11-slim
before_script:
- python -m pip install --upgrade pip
- pip install -r requirements.txt
test:
extends: .python_job
stage: test
script:
- pytest -q
postgres-readiness:
extends: .python_job
stage: test
services:
- name: postgres:16-alpine
alias: postgres
- name: rabbitmq:3-management
alias: rabbitmq
variables:
POSTGRES_DB: mvpcc
POSTGRES_USER: mvp
POSTGRES_PASSWORD: mvp
DATABASE_URL: postgresql://mvp:mvp@postgres:5432/mvpcc
SCHEMA_MANAGEMENT_MODE: migrations
CC_DATA_DIR: .testdata_pg
EVENT_BUS_ENABLED: "0"
EVENT_BUS_URL: amqp://guest:guest@rabbitmq:5672/
APP_TOKEN_SECRET: ci-secret
ALLOW_LEGACY_HEADER_AUTH: "1"
TELEGRAM_BOT_ENABLED: "0"
WHATSAPP_OUTBOUND_ENABLED: "0"
AI_PROVIDER: stub
AI_TELEGRAM_ENABLED: "0"
AI_WHATSAPP_ENABLED: "0"
AI_VOICE_ENABLED: "0"
script:
- python scripts/migrate_core_db.py
- pytest -q tests/test_postgres_readiness.py
.docker_build:
image: docker:27
services:
- name: docker:27-dind
alias: docker
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_COMMIT_TAG'
build-app-image:
extends: .docker_build
stage: build
needs:
- test
- postgres-readiness
script:
- docker build -t "$APP_IMAGE" .
- docker push "$APP_IMAGE"
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
docker tag "$APP_IMAGE" "$APP_IMAGE_REPOSITORY:latest"
docker push "$APP_IMAGE_REPOSITORY:latest"
fi
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker tag "$APP_IMAGE" "$APP_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
docker push "$APP_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
fi
build-asterisk-image:
extends: .docker_build
stage: build
needs:
- test
- postgres-readiness
script:
- docker build -f deployment/asterisk/Dockerfile -t "$ASTERISK_IMAGE" .
- docker push "$ASTERISK_IMAGE"
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
docker tag "$ASTERISK_IMAGE" "$ASTERISK_IMAGE_REPOSITORY:latest"
docker push "$ASTERISK_IMAGE_REPOSITORY:latest"
fi
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker tag "$ASTERISK_IMAGE" "$ASTERISK_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
docker push "$ASTERISK_IMAGE_REPOSITORY:$CI_COMMIT_TAG"
fi
deploy-production:
stage: deploy
image: alpine:3.21
needs:
- build-app-image
- build-asterisk-image
environment:
name: production
before_script:
- apk add --no-cache openssh-client rsync
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- printf '%s\n' "$DEPLOY_SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
script:
- test -n "$DEPLOY_HOST"
- test -n "$DEPLOY_USER"
- test -n "$DEPLOY_PATH"
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEPLOY_PATH/deployment'"
- rsync -av deployment/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/deployment/"
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"mkdir -p '$DEPLOY_PATH/.data_local/generated_ivr_yandex/ivr' '$DEPLOY_PATH/.data_local/recordings' '$DEPLOY_PATH/.asterisk_assets'"
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"printf 'APP_IMAGE=%s\nASTERISK_IMAGE=%s\n' '$APP_IMAGE' '$ASTERISK_IMAGE' > '$DEPLOY_PATH/deployment/.env.images'"
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"echo '$CI_REGISTRY_PASSWORD' | docker login '$CI_REGISTRY' -u '$CI_REGISTRY_USER' --password-stdin && \
cd '$DEPLOY_PATH/deployment' && \
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"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
allow_failure: false
+10
View File
@@ -0,0 +1,10 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY . /app
ENV APP_MODULE=services.auth_service.app:app
EXPOSE 8000 9019
CMD ["sh", "-c", "uvicorn ${APP_MODULE} --host 0.0.0.0 --port 8000"]
+373
View File
@@ -0,0 +1,373 @@
# MVP Contact Center (KZ) - Sequential 4-Stage Implementation
This repository bootstraps the agreed MVP implementation for the contact center platform.
## What is implemented
- Stage 1 foundation artifacts
- API gateway skeleton
- local RBAC auth service
- users, health, audit contracts
- Kubernetes + Helm + CI skeleton
- observability baseline docs and manifests
- Stage 2 operator core APIs
- customers
- interactions and escalation
- queue/routing rules
- voice events adapter (Asterisk-facing)
- Telegram webhook adapter
- Webchat intake adapter
- Stage 3 supervisor/reporting/KB-lite APIs
- knowledge base categories/articles/search
- KPI aggregation (SL, ASA, AHT, Abandon, FCR)
- supervisor realtime endpoints
- Stage 4 pilot hardening package
- gate checklists
- UAT package and templates
- runbooks for backup/restore and pilot operations
- Wave 2 Track 1-2 baseline
- signed bearer-token auth + optional Keycloak OIDC login
- first additional digital channels: Webchat and Email
- Wave 2 Track 4 baseline
- recording-service for managed local voice recordings
- supervisor playback/download/archive flow
- Wave 2 Track 5 baseline
- ivr-service for DTMF voice flows and IVR runtime sessions
- IVR-aware route preview and admin-side IVR management
- Wave 2 Track 6 baseline
- expanded KPI catalog with channel-aware metrics and coverage endpoint
- reporting export now includes channel and agent dimensions
- Wave 2 Track 7 baseline
- production-like Helm scale profile for K8s on-prem
- Postgres-first DB pooling and formal load/hardening validation scripts
- Wave 2 Track 8 baseline
- RabbitMQ-backed event bus with transactional outbox
- first consumers wired for audit and reporting
## Architecture
- Microservices (FastAPI), each service isolated in `services/*`
- API contracts in `contracts/openapi`
- Event schemas in `contracts/events`
- Deployment:
- Local compose: `deployment/docker-compose.yml`
- Kubernetes manifests: `deployment/kubernetes`
- Helm chart: `deployment/helm`
- GitLab registry server deploy: `deployment/docker-compose.server.registry.yml`
- Service state persistence:
- SQL storage via `DATABASE_URL` for all services (SQLite by default, PostgreSQL supported)
- production-like K8s scale validation is PostgreSQL-only
## Quick start (local)
1. Install dependencies
```bash
python -m pip install -r requirements.txt
```
2. Run tests
```bash
pytest -q
```
3. Apply DB migrations
```bash
python scripts/migrate_core_db.py
```
3. Start individual services (example)
```bash
uvicorn services.auth_service.app:app --reload --port 8001
uvicorn services.interaction_service.app:app --reload --port 8004
```
4. Start local gateway
```bash
uvicorn gateway.app:app --reload --port 8080
```
Operator UI is available at:
```text
http://localhost:8080/operator
```
Additional dedicated Wave 2 shells:
```text
http://localhost:8080/supervisor
http://localhost:8080/admin
```
5. Optional helper scripts (PowerShell)
```powershell
powershell -ExecutionPolicy Bypass -File scripts\run_all_local.ps1
powershell -ExecutionPolicy Bypass -File scripts\stop_all_local.ps1
powershell -ExecutionPolicy Bypass -File scripts\prepare_demo.ps1
powershell -ExecutionPolicy Bypass -File scripts\backup_data.ps1
powershell -ExecutionPolicy Bypass -File scripts\restore_data.ps1 -BackupZip e:\Zhan\backups\mvp_cc_data_YYYYMMDD_HHMMSS.zip
powershell -ExecutionPolicy Bypass -File scripts\clean_workspace.ps1
python scripts\local_stack.py status
python scripts\demo_seed.py
python scripts\oidc_smoke.py --base-url http://localhost:8080
python scripts\load_test.py --base-url http://localhost:8080 --profile baseline_100_100
python scripts\track7_check.py --namespace mvp-cc --report-dir .artifacts\track7\<timestamp>
python scripts\postgres_dev_preflight.py --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
python scripts\live_smoke_gate12.py
python scripts\live_smoke_gate12.py --database-url postgresql://mvp:mvp@localhost:5432/mvpcc
python scripts\track9_preflight.py --base-url http://127.0.0.1:8080 --check-sftp
powershell -ExecutionPolicy Bypass -File scripts\start_track9_qa.ps1
python scripts\asterisk_lab_smoke.py --base-url http://127.0.0.1:8080 --database-url <database-url>
python scripts\track9_check.py --base-url http://127.0.0.1:8080 --database-url <database-url> --require-recording
python scripts\track9_collect_evidence.py --base-url http://127.0.0.1:8080 --database-url <database-url> --run-checks --require-recording
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-host> -AmiUser <ami-user> -AmiSecret <ami-secret> -SftpHost <asterisk-host> -SftpUser <sftp-user> -SftpPassword <sftp-password> -QueueId <queue-id> -AppTokenSecret <app-token-secret>
python scripts\gate3_check.py --auto-start
python scripts\gate4_check.py
python scripts\uat_preflight.py --auto-start
python scripts\uat_dry_run.py --auto-start --update-defect-register
python scripts\uat_manual_prepare.py --environment-url http://<pilot-gateway>:8080
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>
```
## Demo-ready local run
Use one command to start the stack, run smoke checks, and preload demo data:
```powershell
powershell -ExecutionPolicy Bypass -File scripts\prepare_demo.ps1
```
This starts every service in the background without opening extra PowerShell windows, writes logs to `.local_stack\logs`, and seeds:
- a demo customer
- a closed escalated voice interaction
- an active follow-up interaction
- voice + Telegram integration events
- one `recording.ready` event imported into managed storage
- one IVR flow with a completed demo session and route preview
- KB demo article
- supervisor snapshot
- KPI sample rows
Stop the stack with:
```powershell
powershell -ExecutionPolicy Bypass -File scripts\stop_all_local.ps1
```
Local runtime state under `.data*`, `.local_stack`, and `.artifacts` is disposable.
To reset the local workspace back to a clean baseline, run:
```powershell
powershell -ExecutionPolicy Bypass -File scripts\clean_workspace.ps1
```
Relevant PostgreSQL cutover runbooks:
- [local-run.md](/e:/Zhan/docs/runbooks/local-run.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)
- [deployment-onprem.md](/e:/Zhan/docs/runbooks/deployment-onprem.md)
## Wave 2 enterprise auth baseline (Track 1)
Track 1 introduces signed application bearer tokens and optional Keycloak-backed OIDC login.
Default local/demo mode:
- `OIDC_ENABLED=0`
- `ALLOW_LEGACY_HEADER_AUTH=1`
Enterprise-shaped mode:
- `OIDC_ENABLED=1`
- `ALLOW_LEGACY_HEADER_AUTH=0`
- set `APP_TOKEN_SECRET`
- set `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_REDIRECT_URI`
Quick checks:
```powershell
python scripts\migrate_core_db.py
python scripts\oidc_smoke.py --base-url http://localhost:8080
python scripts\oidc_smoke.py --base-url http://localhost:8080 --require-enabled
```
Detailed rollout guidance:
- `docs/runbooks/keycloak-oidc.md`
- `docs/runbooks/deployment-onprem.md`
## Wave 2 Track 3 dedicated shells
Track 3 starts moving critical workflows out of the single operator shell:
- `/supervisor` for realtime, KPI, agent status, and queue snapshot controls
- `/admin` for users, queues, and routing previews
## Wave 2 Track 4 recordings
Track 4 introduces the first voice recording lifecycle:
- `recording-service` stores metadata for voice recordings
- audio files are copied into managed local storage
- `/supervisor` can import, preview, download, and archive recordings
Operational runbook:
- `docs/runbooks/recordings-local.md`
## Wave 2 Track 5 IVR
Track 5 introduces the first IVR runtime for voice:
- `ivr-service` stores IVR flow configs and runtime sessions
- DTMF steps can complete a session into an `ivr.completed` event
- `/admin` can create flows, start sessions, send digits, and preview routing with `ivr_session_id`
Operational runbook:
- `docs/runbooks/ivr-local.md`
## Wave 2 Track 6 KPI Expansion
Track 6 expands reporting beyond the MVP core subset:
- `reporting-service` now stores `channel` and `agent_id`
- `/reports/kpi` adds expanded metrics and channel breakdowns
- `/reports/coverage` exposes the implemented KPI catalog
Reference:
- `docs/kpi/definitions.md`
## Wave 2 Track 7 Scale-Up and Hardening
Track 7 adds the first production-shaped scale profile:
- Helm is now the canonical path for K8s scale validation
- `DATABASE_URL` is required for the `scale500` profile
- runtime DB pooling is configurable with:
- `DB_POOL_SIZE`
- `DB_MAX_OVERFLOW`
- `DB_POOL_TIMEOUT_SECONDS`
- `DB_POOL_RECYCLE_SECONDS`
- `scripts/load_test.py` now runs mixed load profiles and writes evidence to `.artifacts/track7/*`
- `scripts/track7_check.py` validates the load report plus K8s pod/HPA health
Operational runbook:
- `docs/runbooks/track7-scale-validation.md`
## Wave 2 Track 8 Event Bus
Track 8 adds the first asynchronous integration backbone:
- `event_outbox` / `event_inbox` tables for reliable publication and idempotent consumption
- `event-bus-service` for outbox dispatch and operational retry
- RabbitMQ as the first canonical message bus
- first consumers:
- `audit-service`
- `reporting-service`
Operational runbook:
- `docs/runbooks/event-bus-local.md`
## Wave 2 Track 9 Asterisk Lab Integration
Track 9 adds the first real telephony bridge toward Asterisk:
- `asterisk-bridge-service` listens to AMI `UserEvent` messages with the `MVPCC` prefix
- Asterisk `QueueCode` values are mapped to platform `queue_id` values through config
- `MVPCCCallStarted` creates a real `voice` interaction and forwards `call.started`
- `MVPCCCallEnded` forwards `call.ended` and keeps the interaction linked
- `MVPCCRecordingReady` can fetch a recording from a Linux VM over `SFTP` and upload it into `recording-service`
- bridge internal auth mode is configurable:
- `ASTERISK_BRIDGE_AUTH_MODE=legacy_headers` (Track 9 QA baseline)
- `ASTERISK_BRIDGE_AUTH_MODE=bearer_first|bearer` (Track 9.1 hardening)
- strict service-token hardening adds subject allowlists:
- `VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS`
- `RECORDING_IMPORT_TRUSTED_SERVICE_SUBJECTS`
- `RECORDING_IMPORT_ALLOW_ADMIN=0` in strict mode
Repository assets for the Linux VM lab:
- `deployment/asterisk/pjsip.conf`
- `deployment/asterisk/extensions.conf`
- `deployment/asterisk/manager.conf`
Operational runbook:
- `docs/runbooks/asterisk-lab-linux-vm.md`
- `docs/runbooks/.env.production.checklist.md`
- `deployment/helm/values.track9-strict.yaml` (strict bridge auth overlay)
- `docs/runbooks/track9-2-production-cutover.md` (controlled production cutover)
Track 9 QA config artifacts:
- `.env.production.template`
- `docs/acceptance/track9/track9-acceptance.md`
## Wave 2 Track 11 Live Operator Voice Control
Track 11 extends the Asterisk bridge with operator call control:
- new bridge 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 gets a `Живые звонки` block with claim/hangup/blind-transfer controls
- own-call RBAC guard for operator role
- additive voice events:
- `call.connected`
- `call.transferred`
Runbook and acceptance templates:
- `docs/runbooks/track11-live-operator-call-control.md`
- `docs/acceptance/track11/README.md`
- `docs/acceptance/track11/track11-acceptance.template.md`
## PostgreSQL run (docker-compose)
```bash
cd deployment
docker compose up -d postgres
cd ..
python scripts/migrate_core_db.py
```
## Shared Docker host PostgreSQL for call-center
When the server already hosts other Docker projects and the running `call-center` stack is still SQLite-backed, use the isolated server-side PostgreSQL asset instead of reusing an existing database:
- compose: `deployment/docker-compose.postgres.server.yml`
- template: `.env.postgres.server.template`
- runbook: `docs/runbooks/postgres-server-docker.md`
To bring up a second PostgreSQL-backed application stack in parallel on the same host:
- compose: `deployment/docker-compose.parallel.server.yml`
- template: `.env.postgres.parallel.server.template`
- runbook: `docs/runbooks/postgres-server-parallel-stack.md`
## Stage-by-stage execution
- Stage 1: see `docs/roadmap/01-foundation.md`
- Stage 2: see `docs/roadmap/02-operator-core.md`
- Stage 3: see `docs/roadmap/03-supervisor-reporting-kb.md`
- Stage 4: see `docs/roadmap/04-pilot-hardening.md`
Every stage has a gate checklist in `docs/gates`.
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "AgentStateChanged",
"type": "object",
"required": ["event", "agent_id", "state", "created_at"],
"properties": {
"event": { "const": "agent.state.changed" },
"agent_id": { "type": "string" },
"state": { "type": "string", "enum": ["READY", "BUSY", "BREAK", "OFFLINE"] },
"queue_id": { "type": ["string", "null"] },
"created_at": { "type": "string", "format": "date-time" }
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CallConnected",
"type": "object",
"required": ["event", "call_id", "created_at"],
"properties": {
"event": { "const": "call.connected" },
"call_id": { "type": "string" },
"interaction_id": { "type": ["string", "null"] },
"operator_extension": { "type": ["string", "null"] },
"claimed_by_user": { "type": ["string", "null"] },
"created_at": { "type": "string", "format": "date-time" }
}
}
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CallRecordingReady",
"type": "object",
"required": ["event", "call_id", "recording_path", "duration_seconds", "created_at"],
"properties": {
"event": { "const": "call.recording.ready" },
"call_id": { "type": "string" },
"interaction_id": { "type": ["string", "null"] },
"recording_path": { "type": "string" },
"duration_seconds": { "type": "integer", "minimum": 0 },
"created_at": { "type": "string", "format": "date-time" }
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CallTransferred",
"type": "object",
"required": ["event", "call_id", "created_at"],
"properties": {
"event": { "const": "call.transferred" },
"call_id": { "type": "string" },
"interaction_id": { "type": ["string", "null"] },
"target_type": { "type": ["string", "null"], "enum": ["extension", "queue_code", null] },
"target_value": { "type": ["string", "null"] },
"target_extension": { "type": ["string", "null"] },
"actor_user": { "type": ["string", "null"] },
"created_at": { "type": "string", "format": "date-time" }
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "InteractionAssigned",
"type": "object",
"required": ["event", "interaction_id", "assignee", "created_at"],
"properties": {
"event": { "const": "interaction.assigned" },
"interaction_id": { "type": "string" },
"assignee": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" }
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "InteractionClosed",
"type": "object",
"required": ["event", "interaction_id", "resolved_first_contact", "created_at"],
"properties": {
"event": { "const": "interaction.closed" },
"interaction_id": { "type": "string" },
"resolved_first_contact": { "type": "boolean" },
"created_at": { "type": "string", "format": "date-time" }
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "InteractionCreated",
"type": "object",
"required": ["event", "interaction_id", "channel", "created_at"],
"properties": {
"event": { "const": "interaction.created" },
"interaction_id": { "type": "string" },
"channel": { "type": "string", "enum": ["voice", "telegram", "email", "webchat"] },
"queue_id": { "type": ["string", "null"] },
"created_at": { "type": "string", "format": "date-time" }
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "InteractionEscalated",
"type": "object",
"required": ["event", "interaction_id", "target_queue_id", "created_at"],
"properties": {
"event": { "const": "interaction.escalated" },
"interaction_id": { "type": "string" },
"target_queue_id": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" }
}
}
+54
View File
@@ -0,0 +1,54 @@
{
"$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
}
@@ -0,0 +1,60 @@
openapi: 3.0.3
info:
title: Stage 1 Contracts (Auth, Users, Health, Audit)
version: 1.0.0
paths:
/health:
get:
summary: Health check
responses:
'200':
description: OK
/auth/login:
post:
summary: Login
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username: { type: string }
password: { type: string }
responses:
'200':
description: Token response
/users:
get:
summary: List users
responses:
'200':
description: Users list
post:
summary: Create user
responses:
'200':
description: User created
/users/{user_id}:
get:
summary: Get user by id
parameters:
- in: path
name: user_id
required: true
schema: { type: string }
responses:
'200':
description: User data
/audit/events:
get:
summary: List audit events
responses:
'200':
description: Audit list
post:
summary: Create audit event
responses:
'200':
description: Event created
@@ -0,0 +1,68 @@
openapi: 3.0.3
info:
title: Stage 2 Contracts (Operator Core)
version: 1.0.0
paths:
/customers:
get: { summary: Search customers, responses: { '200': { description: OK } } }
post: { summary: Create customer, responses: { '200': { description: OK } } }
/customers/{customer_id}:
get:
summary: Get customer
parameters:
- in: path
name: customer_id
required: true
schema: { type: string }
responses:
'200': { description: OK }
/interactions:
get: { summary: List interactions, responses: { '200': { description: OK } } }
post: { summary: Create interaction, responses: { '200': { description: OK } } }
/interactions/{interaction_id}/assign:
patch:
summary: Assign interaction
parameters:
- in: path
name: interaction_id
required: true
schema: { type: string }
responses:
'200': { description: OK }
/interactions/{interaction_id}/status:
patch:
summary: Update interaction status
parameters:
- in: path
name: interaction_id
required: true
schema: { type: string }
responses:
'200': { description: OK }
/interactions/{interaction_id}/escalate:
post:
summary: Escalate interaction
parameters:
- in: path
name: interaction_id
required: true
schema: { type: string }
responses:
'200': { description: OK }
/queues:
get: { summary: List queues, responses: { '200': { description: OK } } }
post: { summary: Create queue, responses: { '200': { description: OK } } }
/queues/{queue_id}/rules:
patch:
summary: Update queue rules
parameters:
- in: path
name: queue_id
required: true
schema: { type: string }
responses:
'200': { description: OK }
/integrations/voice/events:
post: { summary: Ingest Asterisk event, responses: { '200': { description: OK } } }
/integrations/telegram/webhook:
post: { summary: Ingest Telegram update, responses: { '200': { description: OK } } }
@@ -0,0 +1,54 @@
openapi: 3.0.3
info:
title: Stage 3 Contracts (Supervisor, Reporting, KB-lite)
version: 1.0.0
paths:
/knowledge/categories:
get: { summary: List categories, responses: { '200': { description: OK } } }
post: { summary: Create category, responses: { '200': { description: OK } } }
/knowledge/articles:
post: { summary: Create localized article, responses: { '200': { description: OK } } }
/knowledge/articles/{article_id}:
get:
summary: Get article
parameters:
- in: path
name: article_id
required: true
schema: { type: string }
responses:
'200': { description: OK }
patch:
summary: Update article
parameters:
- in: path
name: article_id
required: true
schema: { type: string }
responses:
'200': { description: OK }
/knowledge/search:
get:
summary: Search localized articles
parameters:
- in: query
name: q
schema: { type: string }
- in: query
name: language
schema: { type: string, enum: [ru, kz] }
- in: query
name: article_group_id
schema: { type: string }
responses:
'200': { description: OK }
/reports/events:
post: { summary: Ingest KPI event, responses: { '200': { description: OK } } }
/reports/kpi:
get: { summary: KPI snapshot, responses: { '200': { description: OK } } }
/reports/export:
get: { summary: Export CSV, responses: { '200': { description: OK } } }
/supervisor/agent-states:
post: { summary: Update agent state, responses: { '200': { description: OK } } }
/supervisor/realtime:
get: { summary: Realtime supervisor view, responses: { '200': { description: OK } } }
+8
View File
@@ -0,0 +1,8 @@
# API Versioning
Current stable baseline: `v1`.
## Rules
- Stage 1-3 may add endpoints only in scope of agreed stage.
- Stage 4 freezes public API contracts and documentation.
- Breaking changes require new major version (`v2`) after pilot acceptance.
+2
View File
@@ -0,0 +1,2 @@
APP_IMAGE=registry.example.com/group/project/app:latest
ASTERISK_IMAGE=registry.example.com/group/project/asterisk:latest
+21
View File
@@ -0,0 +1,21 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
asterisk \
ca-certificates \
curl \
openssl \
python3 \
procps \
uuid-runtime \
&& rm -rf /var/lib/apt/lists/*
COPY deployment/asterisk/entrypoint.sh /usr/local/bin/mvpcc-asterisk-entrypoint.sh
RUN chmod +x /usr/local/bin/mvpcc-asterisk-entrypoint.sh \
&& mkdir -p /config-template /assets /etc/asterisk/keys /var/spool/asterisk/monitor/mvpcc
ENTRYPOINT ["/usr/local/bin/mvpcc-asterisk-entrypoint.sh"]
+74
View File
@@ -0,0 +1,74 @@
# Asterisk Lab Config
This folder contains the lab-grade configuration set for Track 9, Track 11, and Track 14:
- `pjsip.conf`
- `extensions.conf`
- `manager.conf`
- `http.conf`
- `rtp.conf`
These files are templates for a dedicated Linux VM running Asterisk 20 LTS.
Before using them:
1. Replace default passwords and secrets.
2. Confirm the softphone source network.
3. Create the recording directory:
- `/var/spool/asterisk/monitor/mvpcc`
4. Point `ASTERISK_QUEUE_MAP_JSON` in the platform to the matching queue code:
- `voice_lab`
- `voice_lab_ai` for the dedicated AI-first staging entrypoint
- `voice_lab_ivr` for the dedicated IVR staging entrypoint
5. Configure operator extension mapping in bridge env for Track 11 call control:
- `ASTERISK_OPERATOR_EXTENSION_MAP_JSON={"operator_a":"2001","operator_b":"2002"}`
6. Optional transfer map for queue-code based blind transfer:
- `ASTERISK_TRANSFER_TARGET_MAP_JSON={"voice_lab":"2001","voice_lab_ai":"2001","voice_lab_ivr":"7200"}`
7. For IVR-over-Asterisk staging:
- enable `ASTERISK_IVR_FASTAGI_ENABLED=1`
- set `ASTERISK_IVR_FASTAGI_HOST=<bridge-host>`
- set `ASTERISK_IVR_FASTAGI_PORT=4573`
- set `IVR_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:asterisk-bridge`
- provision the referenced `prompt_audio_key` files under the Asterisk sounds directory
- if you want a higher-quality prompt from OpenAI TTS, generate it once and keep it on the server:
- `python scripts/materialize_tts_prompt.py --text "Здравствуйте! Добро пожаловать в контакт-центр. Для русского языка нажмите 1. Қазақ тілі үшін 2 басыңыз." --output /usr/share/asterisk/sounds/en/mvpcc_ivr_language.wav --language ru`
- `python scripts/materialize_tts_prompt.py --text "Для отдела продаж нажмите 1. Для службы поддержки нажмите 2." --output /usr/share/asterisk/sounds/en/mvpcc_ivr_menu_ru.wav --language ru`
- `python scripts/materialize_tts_prompt.py --text "Сату бөлімі үшін 1 басыңыз. Қолдау қызметі үшін 2 басыңыз." --output /usr/share/asterisk/sounds/en/mvpcc_ivr_menu_kz.wav --language kk`
- repeated runs reuse the server cache from `AI_VOICE_TTS_CACHE_DIR` instead of calling OpenAI again for the same text
8. For Track 14 browser softphone QA:
- enable `ASTERISK_WEBRTC_ENABLED=1`
- set `ASTERISK_WEBRTC_WS_URL=wss://<asterisk-host>:8089/ws`
- configure `ASTERISK_BROWSER_SIP_MAP_JSON`
- replace TLS placeholders in `http.conf` with a trusted cert/key pair
9. For Voice AI V1 lab wiring:
- enable `AI_VOICE_ENABLED=1`
- set `AI_VOICE_QUEUE_CONFIG_JSON={"voice_lab_ai":{"mode":"ai_first","agent_profile":"voice_support","handoff_queue_code":"voice_lab","language":"ru"}}`
- set `AI_VOICE_RUNTIME_SERVICE_URL=http://ai-voice-runtime-service:8000`
- keep `AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:ai-voice-runtime`
- load `app_audiosocket.so` on the Asterisk VM before enabling the AI media path
- set `MVPCC_AI_AUDIOSOCKET_SERVICE=<media-host>:<port>` in `extensions.conf` only after the AudioSocket listener is reachable
Entrypoints in `extensions.conf`:
- `7000`: human-only baseline. This path preserves the original Track 9/11 behavior.
- `7100`: AI-first staging path. It emits the normal `MVPCCCallStarted` event with `QueueCode=voice_lab_ai`, then:
- falls back to human dialing immediately if `MVPCC_AI_AUDIOSOCKET_SERVICE` is blank
- otherwise starts `AudioSocket(<uuid>,<host>:<port>)` for the media bridge
- `7200`: IVR staging path. It emits the normal `MVPCCCallStarted` event with `QueueCode=voice_lab_ivr`, then:
- hands control to FastAGI at `MVPCC_IVR_FASTAGI_HOSTPORT`
- routes completed IVR sessions through `mvpcc-transfer`
- falls back to the human baseline if FastAGI does not return a transfer target
Important Voice AI note:
- `AI_VOICE_RUNTIME_SERVICE_URL` is the HTTP control-plane endpoint for platform services.
- `MVPCC_AI_AUDIOSOCKET_SERVICE` is a separate raw AudioSocket address for Asterisk media.
- Do not point `MVPCC_AI_AUDIOSOCKET_SERVICE` at the runtime HTTP port.
Default lab SIP accounts from `pjsip.conf`:
- `1001` (caller)
- `2001` (operator A)
- `2002` (operator B)
Track 14 note:
- `2001` and `2002` are allowed to keep multiple contacts so browser registration can coexist with `MicroSIP/Zoiper` in QA.
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -euo pipefail
template_dir="/config-template"
monitor_dir="/var/spool/asterisk/monitor/mvpcc"
primary_sounds_dir="/usr/share/asterisk/sounds"
legacy_sounds_dir="/var/lib/asterisk/sounds"
keys_dir="/etc/asterisk/keys"
password_1001="${ASTERISK_ENDPOINT_1001_PASSWORD:-${ASTERISK_AMI_SECRET:-ChangeMe-1001}}"
password_2001="${ASTERISK_ENDPOINT_2001_PASSWORD:-${ASTERISK_AMI_SECRET:-ChangeMe-2001}}"
password_2002="${ASTERISK_ENDPOINT_2002_PASSWORD:-${ASTERISK_AMI_SECRET:-ChangeMe-2002}}"
ami_secret="${ASTERISK_AMI_SECRET:-ChangeMe-AmiSecret}"
external_address="${ASTERISK_EXTERNAL_ADDRESS:-92.38.48.166}"
mkdir -p /etc/asterisk "$monitor_dir" "$primary_sounds_dir" "$legacy_sounds_dir" "$keys_dir"
for file in extensions.conf pjsip.conf manager.conf http.conf rtp.conf; do
cp "${template_dir}/${file}" "/etc/asterisk/${file}"
done
export ASTERISK_PASSWORD_1001="$password_1001"
export ASTERISK_PASSWORD_2001="$password_2001"
export ASTERISK_PASSWORD_2002="$password_2002"
export ASTERISK_MANAGER_SECRET="$ami_secret"
export ASTERISK_EXTERNAL_ADDRESS_VALUE="$external_address"
python3 - <<'PY'
from pathlib import Path
import os
replacements = {
"ChangeMe-1001": os.environ["ASTERISK_PASSWORD_1001"],
"ChangeMe-2001": os.environ["ASTERISK_PASSWORD_2001"],
"ChangeMe-2002": os.environ["ASTERISK_PASSWORD_2002"],
"ChangeMe-AmiSecret": os.environ["ASTERISK_MANAGER_SECRET"],
"92.38.48.166": os.environ["ASTERISK_EXTERNAL_ADDRESS_VALUE"],
}
for path in (
Path("/etc/asterisk/pjsip.conf"),
Path("/etc/asterisk/manager.conf"),
):
text = path.read_text(encoding="utf-8")
for old, new in replacements.items():
text = text.replace(old, new)
path.write_text(text, encoding="utf-8")
modules_path = Path("/etc/asterisk/modules.conf")
if modules_path.exists():
text = modules_path.read_text(encoding="utf-8")
if "noload => chan_sip.so" not in text:
stanza = (
"; MVPCC uses PJSIP/WebRTC for browser softphones.\n"
"noload => chan_sip.so\n\n"
)
if "[global]" in text:
text = text.replace("[global]", f"{stanza}[global]", 1)
elif "[modules]" in text:
text = text.rstrip() + "\n\n" + stanza
else:
text = "[modules]\nautoload=yes\n\n" + stanza + text
modules_path.write_text(text, encoding="utf-8")
PY
if [ ! -f "${keys_dir}/asterisk.pem" ] || [ ! -f "${keys_dir}/asterisk.key" ]; then
openssl req \
-x509 \
-nodes \
-newkey rsa:2048 \
-keyout "${keys_dir}/asterisk.key" \
-out "${keys_dir}/asterisk.pem" \
-days 3650 \
-subj "/CN=${external_address}"
fi
if [ -f /assets/mvpcc_ivr_prompt.wav ]; then
for sounds_dir in "$primary_sounds_dir" "$legacy_sounds_dir"; do
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/mvpcc_ivr_prompt.wav"
mkdir -p "${sounds_dir}/en" "${sounds_dir}/ru" "${sounds_dir}/ru_RU"
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/en/mvpcc_ivr_prompt.wav"
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/ru/mvpcc_ivr_prompt.wav"
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/ru_RU/mvpcc_ivr_prompt.wav"
done
fi
if [ -d /assets/ivr ]; then
for sounds_dir in "$primary_sounds_dir" "$legacy_sounds_dir"; do
mkdir -p "${sounds_dir}/ivr"
cp -a /assets/ivr/. "${sounds_dir}/ivr/"
done
fi
chown -R asterisk:asterisk /etc/asterisk "$keys_dir" /var/spool/asterisk/monitor
for sounds_dir in "$primary_sounds_dir" "$legacy_sounds_dir"; do
if [ -w "$sounds_dir" ]; then
chown -R asterisk:asterisk "$sounds_dir"
fi
done
chmod 0755 /var/spool/asterisk /var/spool/asterisk/monitor "$monitor_dir"
exec /usr/sbin/asterisk -f -U asterisk -G asterisk -vvv
+119
View File
@@ -0,0 +1,119 @@
[globals]
MVPCC_QUEUE_CODE=voice_lab
MVPCC_AI_QUEUE_CODE=voice_lab_ai
MVPCC_IVR_QUEUE_CODE=voice_lab_ivr
MVPCC_IVR_FALLBACK_QUEUE_CODE=voice_lab
MVPCC_MONITOR_DIR=/var/spool/asterisk/monitor/mvpcc
MVPCC_HUMAN_DIAL_TARGETS=PJSIP/2001&PJSIP/2002
MVPCC_AI_AUDIOSOCKET_SERVICE=127.0.0.1:9019
MVPCC_IVR_FASTAGI_HOSTPORT=127.0.0.1:4573
[from-softphones]
exten => 7000,1,NoOp(MVPCC human baseline entrypoint)
same => n,Answer()
same => n,Set(MVPCC_QUEUE=${MVPCC_QUEUE_CODE})
same => n,Gosub(mvpcc-call-init,s,1(${EXTEN}))
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
same => n,Hangup()
exten => 7100,1,NoOp(MVPCC AI-first voice lab entrypoint)
same => n,Set(MVPCC_QUEUE=${IF($["${MVPCC_AI_QUEUE_OVERRIDE}" != ""]?${MVPCC_AI_QUEUE_OVERRIDE}:${MVPCC_AI_QUEUE_CODE})})
same => n,GotoIf($["${MVPCC_AI_REUSE_CALL}" = "1"]?ai-reuse,1)
same => n,Answer()
same => n,Gosub(mvpcc-call-init,s,1(${EXTEN}))
same => n,Goto(ai-audiosocket,1)
exten => ai-reuse,1,NoOp(MVPCC AI handoff reusing active IVR call)
same => n,Goto(ai-audiosocket,1)
exten => ai-audiosocket,1,NoOp(MVPCC AI audio bridge start)
same => n,GotoIf($["${MVPCC_AI_AUDIOSOCKET_SERVICE}" = ""]?ai-human-fallback,1)
same => n,Set(MVPCC_MEDIA_UUID=${SHELL(uuidgen | tr -d '\r\n')})
same => n,UserEvent(MVPCCAIAudioBridgeRequested,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},UuidLen:${LEN(${MVPCC_MEDIA_UUID})},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},Channel:${CHANNEL})
same => n,UserEvent(MVPCCAIAudioSocketAttempt,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},UuidLen:${LEN(${MVPCC_MEDIA_UUID})},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},Channel:${CHANNEL})
same => n,TryExec(AudioSocket(${MVPCC_MEDIA_UUID},${MVPCC_AI_AUDIOSOCKET_SERVICE}))
same => n,UserEvent(MVPCCAIAudioSocketResult,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},UuidLen:${LEN(${MVPCC_MEDIA_UUID})},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},TryExecStatus:${TRYSTATUS},Channel:${CHANNEL})
same => n,GotoIf($["${TRYSTATUS}" = "SUCCESS"]?ai-bridge-ended,1)
same => n,UserEvent(MVPCCAIFallbackToHuman,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},Reason:audiosocket_failed,TryExecStatus:${TRYSTATUS},Channel:${CHANNEL})
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
same => n,Hangup()
exten => ai-bridge-ended,1,UserEvent(MVPCCAIAudioBridgeEnded,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},Channel:${CHANNEL})
same => n,Hangup()
exten => ai-human-fallback,1,UserEvent(MVPCCAIFallbackToHuman,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},Reason:audiosocket_service_missing,Channel:${CHANNEL})
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
same => n,Hangup()
exten => 7200,1,NoOp(MVPCC IVR staging entrypoint)
same => n,Answer()
same => n,Set(MVPCC_QUEUE=${MVPCC_IVR_QUEUE_CODE})
same => n,Set(MVPCC_IVR_FALLBACK_QUEUE_CODE=${GLOBAL(MVPCC_IVR_FALLBACK_QUEUE_CODE)})
same => n,Gosub(mvpcc-call-init,s,1(${EXTEN}))
same => n,Set(MVPCC_IVR_RESULT=started)
same => n,Set(MVPCC_IVR_TARGET_QUEUE_CODE=)
same => n,Set(MVPCC_IVR_TARGET_EXTENSION=)
same => n,Set(MVPCC_IVR_SESSION_ID=)
same => n,GotoIf($["${MVPCC_IVR_FASTAGI_HOSTPORT}" = ""]?ivr-human-fallback,1)
same => n,AGI(agi://${MVPCC_IVR_FASTAGI_HOSTPORT}/ivr,${MVPCC_CALL_ID},${MVPCC_LINKED_ID},${MVPCC_QUEUE},${MVPCC_IVR_FALLBACK_QUEUE_CODE},${EXTEN})
same => n,GotoIf($["${MVPCC_IVR_TARGET_EXTENSION}" = ""]?ivr-human-fallback,1)
same => n,Goto(mvpcc-transfer,${MVPCC_IVR_TARGET_EXTENSION},1)
exten => ivr-human-fallback,1,UserEvent(MVPCCIVRFallbackToHuman,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},FallbackQueueCode:${MVPCC_IVR_FALLBACK_QUEUE_CODE},Extension:${MVPCC_ENTRY_EXTENSION},Result:${MVPCC_IVR_RESULT},TargetQueueCode:${MVPCC_IVR_TARGET_QUEUE_CODE},TargetExtension:${MVPCC_IVR_TARGET_EXTENSION},Channel:${CHANNEL})
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
same => n,Hangup()
exten => h,1,StopMixMonitor()
same => n,UserEvent(MVPCCCallEnded,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},HangupCause:${HANGUPCAUSE},DurationSeconds:${CDR(duration)})
same => n,UserEvent(MVPCCRecordingReady,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},RemotePath:${MONITOR_FILENAME},FileName:${MVPCC_RECORD_FILE},MimeType:audio/wav,DurationSeconds:${CDR(duration)})
[mvpcc-call-init]
exten => s,1,Set(MVPCC_ENTRY_EXTENSION=${ARG1})
same => n,Set(MVPCC_CALL_ID=${UNIQUEID})
same => n,Set(MVPCC_LINKED_ID=${CHANNEL(linkedid)})
same => n,Set(MVPCC_RECORD_FILE=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${UNIQUEID}.wav)
same => n,Set(MONITOR_FILENAME=${MVPCC_MONITOR_DIR}/${MVPCC_RECORD_FILE})
same => n,UserEvent(MVPCCCallStarted,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},CallerNumber:${CALLERID(num)},CallerName:${CALLERID(name)},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},Direction:inbound,Channel:${CHANNEL})
same => n,MixMonitor(${MONITOR_FILENAME},a)
same => n,Return()
[mvpcc-connected]
exten => s,1,Set(MVPCC_OPERATOR=${IF($["${ARG3}" != ""]?${ARG3}:${CHANNEL(peername)})})
same => n,Set(MVPCC_OPERATOR=${IF($["${MVPCC_OPERATOR}" = ""]?${EXTEN}:${MVPCC_OPERATOR})})
same => n,UserEvent(MVPCCOperatorConnected,CallID:${ARG1},LinkedID:${ARG2},OperatorExtension:${MVPCC_OPERATOR},Context:${CONTEXT},Channel:${CHANNEL},ClaimedByUser:${MVPCC_OPERATOR})
same => n,Return()
[mvpcc-claim]
exten => _X.,1,NoOp(Claimed call routed to operator ${EXTEN})
same => n,Set(MVPCC_CALL_ID=${CHANNEL(linkedid)})
same => n,Set(MVPCC_LINKED_ID=${CHANNEL(linkedid)})
same => n,Dial(PJSIP/${EXTEN},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}^${EXTEN}))
same => n,Hangup()
[mvpcc-transfer]
exten => 7000,1,NoOp(Queue transfer target ${EXTEN})
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40)
same => n,Hangup()
exten => 7100,1,NoOp(Queue transfer target ${EXTEN})
same => n,Goto(from-softphones,7100,1)
exten => 7200,1,NoOp(Queue transfer target ${EXTEN})
same => n,Goto(from-softphones,7200,1)
exten => _X.,1,NoOp(Blind transfer target ${EXTEN})
same => n,Dial(PJSIP/${EXTEN},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}^${EXTEN}))
same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?transfer-finished,1)
same => n,GotoIf($["${MVPCC_IVR_FALLBACK_QUEUE_CODE}" = ""]?transfer-finished,1)
same => n,Set(MVPCC_IVR_RESULT=fallback_transfer_unavailable)
same => n,Goto(from-softphones,ivr-human-fallback,1)
exten => transfer-finished,1,Hangup()
exten => h,1,StopMixMonitor()
same => n,UserEvent(MVPCCCallEnded,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},HangupCause:${HANGUPCAUSE},DurationSeconds:${CDR(duration)})
same => n,UserEvent(MVPCCRecordingReady,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},RemotePath:${MONITOR_FILENAME},FileName:${MVPCC_RECORD_FILE},MimeType:audio/wav,DurationSeconds:${CDR(duration)})
[mvpcc-operators]
exten => _X.,1,Dial(PJSIP/${EXTEN},30)
same => n,Hangup()
+12
View File
@@ -0,0 +1,12 @@
[general]
enabled=yes
enablestatic=yes
bindaddr=0.0.0.0
bindport=8088
; QA-only TLS placeholders for direct browser SIP over WebSocket.
; Replace with your actual trusted certificate and key paths before enabling WSS.
tlsenable=yes
tlsbindaddr=0.0.0.0:8089
tlscertfile=/etc/asterisk/keys/asterisk.pem
tlsprivatekey=/etc/asterisk/keys/asterisk.key
+11
View File
@@ -0,0 +1,11 @@
[general]
enabled = yes
port = 5038
bindaddr = 0.0.0.0
[mvpcc]
secret = ChangeMe-AmiSecret
deny = 0.0.0.0/0.0.0.0
permit = 0.0.0.0/0.0.0.0
read = system,call,log,verbose,command,agent,user,dtmf,reporting,cdr,dialplan
write = command,call,originate,agent,user
+84
View File
@@ -0,0 +1,84 @@
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0
external_signaling_address=92.38.48.166
external_media_address=92.38.48.166
[transport-wss]
type=transport
protocol=wss
bind=0.0.0.0
external_signaling_address=92.38.48.166
external_media_address=92.38.48.166
[softphone-endpoint](!)
type=endpoint
disallow=all
allow=opus,ulaw,alaw
dtmf_mode=auto_info
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
webrtc=yes
ice_support=yes
rtcp_mux=yes
use_avpf=yes
media_encryption=dtls
dtls_auto_generate_cert=yes
dtls_setup=actpass
media_use_received_transport=yes
media_address=92.38.48.166
[caller-endpoint](!,softphone-endpoint)
context=from-softphones
[operator-endpoint](!,softphone-endpoint)
context=mvpcc-operators
[1001](caller-endpoint)
auth=1001-auth
aors=1001
[1001-auth]
type=auth
auth_type=userpass
username=1001
password=ChangeMe-1001
[1001]
type=aor
max_contacts=2
[2001](operator-endpoint)
auth=2001-auth
aors=2001
[2001-auth]
type=auth
auth_type=userpass
username=2001
password=ChangeMe-2001
[2001]
type=aor
max_contacts=1
remove_existing=yes
support_path=yes
[2002](operator-endpoint)
auth=2002-auth
aors=2002
[2002-auth]
type=auth
auth_type=userpass
username=2002
password=ChangeMe-2002
[2002]
type=aor
max_contacts=1
remove_existing=yes
support_path=yes
+5
View File
@@ -0,0 +1,5 @@
[general]
rtpstart=10000
rtpend=20000
icesupport=true
stunaddr=stun.l.google.com:19302
@@ -0,0 +1,24 @@
services:
asterisk:
image: ${ASTERISK_IMAGE:?Set ASTERISK_IMAGE in deployment/.env.images or shell env}
container_name: call-center-asterisk
restart: unless-stopped
env_file:
- ../.env.production
environment:
ASTERISK_EXTERNAL_ADDRESS: 92.38.48.166
network_mode: host
volumes:
- ./asterisk:/config-template:ro
- ../.asterisk_assets:/assets
- ../.data_local/generated_ivr_yandex/ivr:/usr/share/asterisk/sounds/ivr
- ../.data_local/generated_ivr_yandex/ivr:/var/lib/asterisk/sounds/ivr
- /var/spool/asterisk/monitor:/var/spool/asterisk/monitor
healthcheck:
test:
- CMD-SHELL
- asterisk -rx "core show version" >/dev/null 2>&1
interval: 15s
timeout: 5s
retries: 10
start_period: 20s
@@ -0,0 +1,27 @@
services:
asterisk:
build:
context: ..
dockerfile: deployment/asterisk/Dockerfile
image: call-center-asterisk:local
container_name: call-center-asterisk
restart: unless-stopped
env_file:
- ../.env.production
environment:
ASTERISK_EXTERNAL_ADDRESS: 92.38.48.166
network_mode: host
volumes:
- ./asterisk:/config-template:ro
- ../.codex_tmp:/assets
- ../.data_local/generated_ivr_yandex/ivr:/usr/share/asterisk/sounds/ivr
- ../.data_local/generated_ivr_yandex/ivr:/var/lib/asterisk/sounds/ivr
- /var/spool/asterisk/monitor:/var/spool/asterisk/monitor
healthcheck:
test:
- CMD-SHELL
- asterisk -rx "core show version" >/dev/null 2>&1
interval: 15s
timeout: 5s
retries: 10
start_period: 20s
@@ -0,0 +1,207 @@
name: call-center-pg
x-app-env: &app_env
CC_DATA_DIR: /app/.data_pg_parallel
SCHEMA_MANAGEMENT_MODE: migrations
AUTH_SERVICE_URL: http://auth-service:8000
AUDIT_SERVICE_URL: http://audit-service:8000
CUSTOMER_SERVICE_URL: http://customer-service:8000
INTERACTION_SERVICE_URL: http://interaction-service:8000
ROUTING_SERVICE_URL: http://routing-service:8000
VOICE_ADAPTER_SERVICE_URL: http://voice-adapter-service:8000
RECORDING_SERVICE_URL: http://recording-service:8000
IVR_SERVICE_URL: http://ivr-service:8000
EVENT_BUS_SERVICE_URL: http://event-bus-service:8000
ASTERISK_BRIDGE_SERVICE_URL: http://asterisk-bridge-service:8000
TELEGRAM_ADAPTER_SERVICE_URL: http://telegram-adapter-service:8000
WHATSAPP_ADAPTER_SERVICE_URL: http://whatsapp-adapter-service:8000
AI_ORCHESTRATOR_SERVICE_URL: http://ai-orchestrator-service:8000
AI_VOICE_RUNTIME_SERVICE_URL: http://ai-voice-runtime-service:8000
WEBCHAT_ADAPTER_SERVICE_URL: http://webchat-adapter-service:8000
EMAIL_ADAPTER_SERVICE_URL: http://email-adapter-service:8000
KB_SERVICE_URL: http://kb-service:8000
REPORTING_SERVICE_URL: http://reporting-service:8000
SUPERVISOR_SERVICE_URL: http://supervisor-service:8000
x-service-defaults: &service_defaults
build:
context: ..
image: call-center-app:pg-parallel
restart: unless-stopped
env_file:
- ../.env.production
- ../.env.postgres.parallel.server
environment:
<<: *app_env
volumes:
- ../.data_pg_parallel:/app/.data_pg_parallel
networks:
- default
- postgres
extra_hosts:
- host.docker.internal:host-gateway
healthcheck:
test:
- CMD
- python
- -c
- |
from urllib.request import urlopen
import sys
sys.exit(0 if urlopen("http://127.0.0.1:8000/health", timeout=2).getcode() == 200 else 1)
interval: 15s
timeout: 5s
retries: 10
start_period: 10s
services:
auth-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.auth_service.app:app
audit-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.audit_service.app:app
customer-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.customer_service.app:app
interaction-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.interaction_service.app:app
routing-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.routing_service.app:app
voice-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.voice_adapter_service.app:app
recording-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.recording_service.app:app
CC_RECORDINGS_DIR: /app/.data_pg_parallel/recordings
RECORDING_MAX_BYTES: 26214400
ivr-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ivr_service.app:app
telegram-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.telegram_adapter_service.app:app
whatsapp-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.whatsapp_adapter_service.app:app
ai-orchestrator-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ai_orchestrator_service.app:app
ai-voice-runtime-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ai_voice_runtime_service.app:app
webchat-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.webchat_adapter_service.app:app
email-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.email_adapter_service.app:app
kb-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.kb_service.app:app
reporting-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.reporting_service.app:app
supervisor-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.supervisor_service.app:app
event-bus-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.event_bus_service.app:app
asterisk-bridge-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.asterisk_bridge_service.app:app
ASTERISK_IVR_FASTAGI_HOST: 0.0.0.0
ASTERISK_IVR_FASTAGI_PORT: 4573
api-gateway:
<<: *service_defaults
depends_on:
- auth-service
- audit-service
- customer-service
- interaction-service
- routing-service
- voice-adapter-service
- recording-service
- ivr-service
- telegram-adapter-service
- whatsapp-adapter-service
- ai-orchestrator-service
- ai-voice-runtime-service
- webchat-adapter-service
- email-adapter-service
- kb-service
- reporting-service
- supervisor-service
- event-bus-service
- asterisk-bridge-service
environment:
<<: *app_env
APP_MODULE: gateway.app:app
ports:
- "18080:8000"
networks:
postgres:
external: true
name: call-center-postgres_default
@@ -0,0 +1,21 @@
name: call-center-postgres
services:
postgres:
container_name: call-center-postgres
image: postgres:16-alpine
restart: unless-stopped
env_file:
- ../.env.postgres.server
ports:
- "127.0.0.1:5434:5432"
volumes:
- ../.data_pg:/var/lib/postgresql/data
healthcheck:
test:
- CMD-SHELL
- pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"
interval: 10s
timeout: 5s
retries: 12
start_period: 15s
@@ -0,0 +1,201 @@
name: call-center
x-app-env: &app_env
CC_DATA_DIR: /app/.data_local
DATABASE_URL: sqlite:////app/.data_local/mvp_cc.db
AUTH_SERVICE_URL: http://auth-service:8000
AUDIT_SERVICE_URL: http://audit-service:8000
CUSTOMER_SERVICE_URL: http://customer-service:8000
INTERACTION_SERVICE_URL: http://interaction-service:8000
ROUTING_SERVICE_URL: http://routing-service:8000
VOICE_ADAPTER_SERVICE_URL: http://voice-adapter-service:8000
RECORDING_SERVICE_URL: http://recording-service:8000
IVR_SERVICE_URL: http://ivr-service:8000
EVENT_BUS_SERVICE_URL: http://event-bus-service:8000
ASTERISK_BRIDGE_SERVICE_URL: http://asterisk-bridge-service:8000
TELEGRAM_ADAPTER_SERVICE_URL: http://telegram-adapter-service:8000
WHATSAPP_ADAPTER_SERVICE_URL: http://whatsapp-adapter-service:8000
AI_ORCHESTRATOR_SERVICE_URL: http://ai-orchestrator-service:8000
AI_VOICE_RUNTIME_SERVICE_URL: http://ai-voice-runtime-service:8000
WEBCHAT_ADAPTER_SERVICE_URL: http://webchat-adapter-service:8000
EMAIL_ADAPTER_SERVICE_URL: http://email-adapter-service:8000
KB_SERVICE_URL: http://kb-service:8000
REPORTING_SERVICE_URL: http://reporting-service:8000
SUPERVISOR_SERVICE_URL: http://supervisor-service:8000
x-service-defaults: &service_defaults
image: ${APP_IMAGE:?Set APP_IMAGE in deployment/.env.images or shell env}
restart: unless-stopped
env_file:
- ../.env.production
environment:
<<: *app_env
volumes:
- ../.data_local:/app/.data_local
extra_hosts:
- host.docker.internal:host-gateway
healthcheck:
test:
- CMD
- python
- -c
- |
from urllib.request import urlopen
import sys
sys.exit(0 if urlopen("http://127.0.0.1:8000/health", timeout=2).getcode() == 200 else 1)
interval: 15s
timeout: 5s
retries: 10
start_period: 10s
services:
auth-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.auth_service.app:app
audit-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.audit_service.app:app
customer-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.customer_service.app:app
interaction-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.interaction_service.app:app
routing-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.routing_service.app:app
voice-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.voice_adapter_service.app:app
recording-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.recording_service.app:app
CC_RECORDINGS_DIR: /app/.data_local/recordings
RECORDING_MAX_BYTES: 26214400
ivr-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ivr_service.app:app
telegram-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.telegram_adapter_service.app:app
whatsapp-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.whatsapp_adapter_service.app:app
ai-orchestrator-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ai_orchestrator_service.app:app
ai-voice-runtime-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ai_voice_runtime_service.app:app
ports:
- "9019:9019"
webchat-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.webchat_adapter_service.app:app
email-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.email_adapter_service.app:app
kb-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.kb_service.app:app
reporting-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.reporting_service.app:app
supervisor-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.supervisor_service.app:app
event-bus-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.event_bus_service.app:app
asterisk-bridge-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.asterisk_bridge_service.app:app
ASTERISK_IVR_FASTAGI_HOST: 0.0.0.0
ASTERISK_IVR_FASTAGI_PORT: 4573
ports:
- "4573:4573"
api-gateway:
<<: *service_defaults
container_name: call-center-api-gateway
depends_on:
- auth-service
- audit-service
- customer-service
- interaction-service
- routing-service
- voice-adapter-service
- recording-service
- ivr-service
- telegram-adapter-service
- whatsapp-adapter-service
- ai-orchestrator-service
- ai-voice-runtime-service
- webchat-adapter-service
- email-adapter-service
- kb-service
- reporting-service
- supervisor-service
- event-bus-service
- asterisk-bridge-service
environment:
<<: *app_env
APP_MODULE: gateway.app:app
ports:
- "8080:8000"
+203
View File
@@ -0,0 +1,203 @@
name: call-center
x-app-env: &app_env
CC_DATA_DIR: /app/.data_local
DATABASE_URL: sqlite:////app/.data_local/mvp_cc.db
AUTH_SERVICE_URL: http://auth-service:8000
AUDIT_SERVICE_URL: http://audit-service:8000
CUSTOMER_SERVICE_URL: http://customer-service:8000
INTERACTION_SERVICE_URL: http://interaction-service:8000
ROUTING_SERVICE_URL: http://routing-service:8000
VOICE_ADAPTER_SERVICE_URL: http://voice-adapter-service:8000
RECORDING_SERVICE_URL: http://recording-service:8000
IVR_SERVICE_URL: http://ivr-service:8000
EVENT_BUS_SERVICE_URL: http://event-bus-service:8000
ASTERISK_BRIDGE_SERVICE_URL: http://asterisk-bridge-service:8000
TELEGRAM_ADAPTER_SERVICE_URL: http://telegram-adapter-service:8000
WHATSAPP_ADAPTER_SERVICE_URL: http://whatsapp-adapter-service:8000
AI_ORCHESTRATOR_SERVICE_URL: http://ai-orchestrator-service:8000
AI_VOICE_RUNTIME_SERVICE_URL: http://ai-voice-runtime-service:8000
WEBCHAT_ADAPTER_SERVICE_URL: http://webchat-adapter-service:8000
EMAIL_ADAPTER_SERVICE_URL: http://email-adapter-service:8000
KB_SERVICE_URL: http://kb-service:8000
REPORTING_SERVICE_URL: http://reporting-service:8000
SUPERVISOR_SERVICE_URL: http://supervisor-service:8000
x-service-defaults: &service_defaults
build:
context: ..
image: call-center-app:local
restart: unless-stopped
env_file:
- ../.env.production
environment:
<<: *app_env
volumes:
- ../.data_local:/app/.data_local
extra_hosts:
- host.docker.internal:host-gateway
healthcheck:
test:
- CMD
- python
- -c
- |
from urllib.request import urlopen
import sys
sys.exit(0 if urlopen("http://127.0.0.1:8000/health", timeout=2).getcode() == 200 else 1)
interval: 15s
timeout: 5s
retries: 10
start_period: 10s
services:
auth-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.auth_service.app:app
audit-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.audit_service.app:app
customer-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.customer_service.app:app
interaction-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.interaction_service.app:app
routing-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.routing_service.app:app
voice-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.voice_adapter_service.app:app
recording-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.recording_service.app:app
CC_RECORDINGS_DIR: /app/.data_local/recordings
RECORDING_MAX_BYTES: 26214400
ivr-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ivr_service.app:app
telegram-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.telegram_adapter_service.app:app
whatsapp-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.whatsapp_adapter_service.app:app
ai-orchestrator-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ai_orchestrator_service.app:app
ai-voice-runtime-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.ai_voice_runtime_service.app:app
ports:
- "9019:9019"
webchat-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.webchat_adapter_service.app:app
email-adapter-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.email_adapter_service.app:app
kb-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.kb_service.app:app
reporting-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.reporting_service.app:app
supervisor-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.supervisor_service.app:app
event-bus-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.event_bus_service.app:app
asterisk-bridge-service:
<<: *service_defaults
environment:
<<: *app_env
APP_MODULE: services.asterisk_bridge_service.app:app
ASTERISK_IVR_FASTAGI_HOST: 0.0.0.0
ASTERISK_IVR_FASTAGI_PORT: 4573
ports:
- "4573:4573"
api-gateway:
<<: *service_defaults
container_name: call-center-api-gateway
depends_on:
- auth-service
- audit-service
- customer-service
- interaction-service
- routing-service
- voice-adapter-service
- recording-service
- ivr-service
- telegram-adapter-service
- whatsapp-adapter-service
- ai-orchestrator-service
- ai-voice-runtime-service
- webchat-adapter-service
- email-adapter-service
- kb-service
- reporting-service
- supervisor-service
- event-bus-service
- asterisk-bridge-service
environment:
<<: *app_env
APP_MODULE: gateway.app:app
ports:
- "8080:8000"
+325
View File
@@ -0,0 +1,325 @@

x-app-env: &app_env
CC_DATA_DIR: /app/.data
DATABASE_URL: postgresql://mvp:mvp@postgres:5432/mvpcc
SCHEMA_MANAGEMENT_MODE: migrations
EVENT_BUS_ENABLED: "1"
EVENT_BUS_URL: amqp://guest:guest@rabbitmq:5672/
EVENT_BUS_EXCHANGE: mvpcc.domain.events
EVENT_BUS_DISPATCH_BATCH_SIZE: "50"
EVENT_BUS_MAX_ATTEMPTS: "5"
EVENT_BUS_POLL_INTERVAL_SECONDS: "2"
EVENT_BUS_CONSUMER_ENABLED: "1"
EVENT_BUS_AUDIT_QUEUE: mvpcc.audit.events
EVENT_BUS_REPORTING_QUEUE: mvpcc.reporting.events
ASTERISK_BRIDGE_ENABLED: "0"
ASTERISK_AMI_EVENT_PREFIX: MVPCC
ASTERISK_QUEUE_MAP_JSON: "{}"
AI_VOICE_RUNTIME_SERVICE_URL: http://ai-voice-runtime-service:8000
AI_VOICE_ENABLED: "0"
AI_VOICE_QUEUE_CONFIG_JSON: "{}"
AI_VOICE_ASR_PROVIDER: openai
AI_VOICE_TTS_PROVIDER: openai
AI_VOICE_AUDIOSOCKET_ENABLED: "1"
AI_VOICE_AUDIOSOCKET_HOST: 0.0.0.0
AI_VOICE_AUDIOSOCKET_PORT: "9019"
AI_VOICE_ASR_MODEL: gpt-4o-mini-transcribe
AI_VOICE_TTS_MODEL: gpt-4o-mini-tts
AI_VOICE_TTS_VOICE: alloy
AI_VOICE_TTS_CACHE_ENABLED: "1"
AI_VOICE_TTS_CACHE_DIR: /app/.data/ai_voice_tts_cache
AI_VOICE_VAD_MIN_SPEECH_MS: "300"
AI_VOICE_VAD_TRAILING_SILENCE_MS: "700"
AI_VOICE_TURN_MAX_MS: "10000"
AI_VOICE_MEDIA_IDLE_TIMEOUT_SECONDS: "15"
AI_VOICE_MAX_CONTEXT_SEGMENTS: "8"
AI_VOICE_HANDOFF_TIMEOUT_SECONDS: "8"
AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS: svc:ai-voice-runtime
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: mvpcc
POSTGRES_USER: mvp
POSTGRES_PASSWORD: mvp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
auth-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.auth_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8001:8000"
audit-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.audit_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8002:8000"
customer-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.customer_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8003:8000"
interaction-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.interaction_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8004:8000"
routing-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.routing_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8005:8000"
voice-adapter-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.voice_adapter_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8006:8000"
recording-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.recording_service.app:app
CC_RECORDINGS_DIR: /app/.data/recordings
RECORDING_MAX_BYTES: 26214400
volumes:
- ../.data:/app/.data
ports:
- "8013:8000"
ivr-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.ivr_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8014:8000"
telegram-adapter-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.telegram_adapter_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8007:8000"
whatsapp-adapter-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.whatsapp_adapter_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8019:8000"
ai-orchestrator-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.ai_orchestrator_service.app:app
TELEGRAM_ADAPTER_SERVICE_URL: http://telegram-adapter-service:8000
WHATSAPP_ADAPTER_SERVICE_URL: http://whatsapp-adapter-service:8000
INTERACTION_SERVICE_URL: http://interaction-service:8000
volumes:
- ../.data:/app/.data
ports:
- "8017:8000"
ai-voice-runtime-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.ai_voice_runtime_service.app:app
AI_ORCHESTRATOR_SERVICE_URL: http://ai-orchestrator-service:8000
ASTERISK_BRIDGE_SERVICE_URL: http://asterisk-bridge-service:8000
volumes:
- ../.data:/app/.data
ports:
- "8018:8000"
- "9019:9019"
webchat-adapter-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.webchat_adapter_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8011:8000"
email-adapter-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.email_adapter_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8012:8000"
kb-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.kb_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8008:8000"
reporting-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.reporting_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8009:8000"
supervisor-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.supervisor_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8010:8000"
event-bus-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.event_bus_service.app:app
volumes:
- ../.data:/app/.data
ports:
- "8015:8000"
asterisk-bridge-service:
build: ..
depends_on: [postgres, rabbitmq]
environment:
<<: *app_env
APP_MODULE: services.asterisk_bridge_service.app:app
INTERACTION_SERVICE_URL: http://interaction-service:8000
VOICE_ADAPTER_SERVICE_URL: http://voice-adapter-service:8000
RECORDING_SERVICE_URL: http://recording-service:8000
IVR_SERVICE_URL: http://ivr-service:8000
volumes:
- ../.data:/app/.data
ports:
- "8016:8000"
- "4573:4573"
api-gateway:
build: ..
depends_on:
- auth-service
- audit-service
- customer-service
- interaction-service
- routing-service
- voice-adapter-service
- recording-service
- ivr-service
- telegram-adapter-service
- whatsapp-adapter-service
- ai-orchestrator-service
- ai-voice-runtime-service
- webchat-adapter-service
- email-adapter-service
- kb-service
- reporting-service
- supervisor-service
- event-bus-service
- asterisk-bridge-service
environment:
APP_MODULE: gateway.app:app
AUTH_SERVICE_URL: http://auth-service:8000
AUDIT_SERVICE_URL: http://audit-service:8000
CUSTOMER_SERVICE_URL: http://customer-service:8000
INTERACTION_SERVICE_URL: http://interaction-service:8000
ROUTING_SERVICE_URL: http://routing-service:8000
VOICE_ADAPTER_SERVICE_URL: http://voice-adapter-service:8000
RECORDING_SERVICE_URL: http://recording-service:8000
IVR_SERVICE_URL: http://ivr-service:8000
EVENT_BUS_SERVICE_URL: http://event-bus-service:8000
ASTERISK_BRIDGE_SERVICE_URL: http://asterisk-bridge-service:8000
TELEGRAM_ADAPTER_SERVICE_URL: http://telegram-adapter-service:8000
WHATSAPP_ADAPTER_SERVICE_URL: http://whatsapp-adapter-service:8000
AI_ORCHESTRATOR_SERVICE_URL: http://ai-orchestrator-service:8000
WEBCHAT_ADAPTER_SERVICE_URL: http://webchat-adapter-service:8000
EMAIL_ADAPTER_SERVICE_URL: http://email-adapter-service:8000
KB_SERVICE_URL: http://kb-service:8000
REPORTING_SERVICE_URL: http://reporting-service:8000
SUPERVISOR_SERVICE_URL: http://supervisor-service:8000
ports:
- "8080:8000"
volumes:
pgdata:
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: mvp-cc-platform
description: Helm chart for MVP Contact Center microservices
type: application
version: 0.1.0
appVersion: "1.0.0"
@@ -0,0 +1,48 @@
{{- $databaseUrl := $.Values.global.databaseUrl | default "" }}
{{- $schemaManagementMode := $.Values.global.schemaManagementMode | default "legacy" }}
{{- $runMigrations := and $.Values.migrations.enabled (eq $schemaManagementMode "migrations") (or $.Values.profiles.scale500.enabled $databaseUrl) }}
{{- if $runMigrations }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrate
namespace: {{ $.Values.namespace }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": "{{ $.Values.migrations.hookDeletePolicy }}"
spec:
backoffLimit: {{ $.Values.migrations.backoffLimit }}
activeDeadlineSeconds: {{ $.Values.migrations.activeDeadlineSeconds }}
ttlSecondsAfterFinished: {{ $.Values.migrations.ttlSecondsAfterFinished }}
template:
metadata:
labels:
app: {{ .Release.Name }}-db-migrate
spec:
restartPolicy: Never
{{- if $.Values.global.runtimeClass }}
runtimeClassName: {{ $.Values.global.runtimeClass }}
{{- end }}
containers:
- name: migrate
image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag }}"
imagePullPolicy: {{ $.Values.global.imagePullPolicy }}
command:
- python
- scripts/migrate_core_db.py
env:
- name: CC_DATA_DIR
value: /var/lib/mvp-cc/data
- name: DATABASE_URL
value: "{{ if $.Values.profiles.scale500.enabled }}{{ required "global.databaseUrl is required when profiles.scale500.enabled=true" $.Values.global.databaseUrl }}{{ else }}{{ $databaseUrl }}{{ end }}"
- name: SCHEMA_MANAGEMENT_MODE
value: "{{ $schemaManagementMode }}"
resources:
requests:
cpu: {{ $.Values.migrations.resources.requests.cpu }}
memory: {{ $.Values.migrations.resources.requests.memory }}
limits:
cpu: {{ $.Values.migrations.resources.limits.cpu }}
memory: {{ $.Values.migrations.resources.limits.memory }}
{{- end }}
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.namespace }}
+46
View File
@@ -0,0 +1,46 @@
{{- if .Values.rabbitmq.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: rabbitmq
namespace: {{ .Values.namespace }}
spec:
replicas: {{ .Values.rabbitmq.replicas }}
selector:
matchLabels:
app: rabbitmq
template:
metadata:
labels:
app: rabbitmq
spec:
containers:
- name: rabbitmq
image: {{ .Values.rabbitmq.image }}
env:
- name: RABBITMQ_DEFAULT_USER
value: "{{ .Values.rabbitmq.user }}"
- name: RABBITMQ_DEFAULT_PASS
value: "{{ .Values.rabbitmq.password }}"
ports:
- name: amqp
containerPort: {{ .Values.rabbitmq.servicePort }}
- name: management
containerPort: {{ .Values.rabbitmq.managementPort }}
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
namespace: {{ .Values.namespace }}
spec:
selector:
app: rabbitmq
ports:
- name: amqp
port: {{ .Values.rabbitmq.servicePort }}
targetPort: {{ .Values.rabbitmq.servicePort }}
- name: management
port: {{ .Values.rabbitmq.managementPort }}
targetPort: {{ .Values.rabbitmq.managementPort }}
{{- end }}
+404
View File
@@ -0,0 +1,404 @@
{{- range $name, $svc := .Values.services }}
{{- $databaseUrl := $.Values.global.databaseUrl | default "" }}
{{- $schemaManagementMode := $.Values.global.schemaManagementMode | default "legacy" }}
{{- $isGateway := default false $svc.gateway }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Values.namespace }}
spec:
replicas: {{ $svc.replicas }}
selector:
matchLabels:
app: {{ $name }}
template:
metadata:
labels:
app: {{ $name }}
spec:
terminationGracePeriodSeconds: 30
{{- if $.Values.global.runtimeClass }}
runtimeClassName: {{ $.Values.global.runtimeClass }}
{{- end }}
{{- if gt (int $svc.replicas) 1 }}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: {{ $name }}
{{- end }}
containers:
- name: app
image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag }}"
imagePullPolicy: {{ $.Values.global.imagePullPolicy }}
env:
- name: APP_MODULE
value: "{{ $svc.module }}"
- name: CC_DATA_DIR
value: /var/lib/mvp-cc/data
- name: APP_TOKEN_SECRET
value: "{{ $.Values.auth.appTokenSecret }}"
- name: APP_TOKEN_TTL_SECONDS
value: "{{ $.Values.auth.appTokenTtlSeconds }}"
- name: ALLOW_LEGACY_HEADER_AUTH
value: "{{ $.Values.auth.allowLegacyHeaderAuth }}"
{{- if or $.Values.profiles.scale500.enabled $databaseUrl }}
- name: DATABASE_URL
value: "{{ if $.Values.profiles.scale500.enabled }}{{ required "global.databaseUrl is required when profiles.scale500.enabled=true" $.Values.global.databaseUrl }}{{ else }}{{ $databaseUrl }}{{ end }}"
{{- end }}
- name: SCHEMA_MANAGEMENT_MODE
value: "{{ $schemaManagementMode }}"
- name: DB_POOL_SIZE
value: "{{ $.Values.db.poolSize }}"
- name: DB_MAX_OVERFLOW
value: "{{ $.Values.db.maxOverflow }}"
- name: DB_POOL_TIMEOUT_SECONDS
value: "{{ $.Values.db.poolTimeoutSeconds }}"
- name: DB_POOL_RECYCLE_SECONDS
value: "{{ $.Values.db.poolRecycleSeconds }}"
- name: EVENT_BUS_ENABLED
value: "{{ $.Values.eventBus.enabled }}"
- name: EVENT_BUS_URL
value: "{{ $.Values.eventBus.url }}"
- name: EVENT_BUS_EXCHANGE
value: "{{ $.Values.eventBus.exchange }}"
- name: EVENT_BUS_DISPATCH_BATCH_SIZE
value: "{{ $.Values.eventBus.dispatchBatchSize }}"
- name: EVENT_BUS_MAX_ATTEMPTS
value: "{{ $.Values.eventBus.maxAttempts }}"
- name: EVENT_BUS_POLL_INTERVAL_SECONDS
value: "{{ $.Values.eventBus.pollIntervalSeconds }}"
- name: EVENT_BUS_CONSUMER_ENABLED
value: "{{ $.Values.eventBus.consumerEnabled }}"
- name: EVENT_BUS_AUDIT_QUEUE
value: "{{ $.Values.eventBus.auditQueue }}"
- name: EVENT_BUS_REPORTING_QUEUE
value: "{{ $.Values.eventBus.reportingQueue }}"
{{- if eq $name "asterisk-bridge-service" }}
- name: ASTERISK_BRIDGE_ENABLED
value: "{{ $.Values.asteriskBridge.enabled }}"
- name: ASTERISK_AMI_HOST
value: "{{ $.Values.asteriskBridge.amiHost }}"
- name: ASTERISK_AMI_PORT
value: "{{ $.Values.asteriskBridge.amiPort }}"
- name: ASTERISK_AMI_USERNAME
value: "{{ $.Values.asteriskBridge.amiUsername }}"
- name: ASTERISK_AMI_SECRET
value: "{{ $.Values.asteriskBridge.amiSecret }}"
- name: ASTERISK_AMI_EVENT_PREFIX
value: "{{ $.Values.asteriskBridge.amiEventPrefix }}"
- name: ASTERISK_QUEUE_MAP_JSON
value: '{{ $.Values.asteriskBridge.queueMapJson }}'
- name: ASTERISK_SFTP_HOST
value: "{{ $.Values.asteriskBridge.sftpHost }}"
- name: ASTERISK_SFTP_PORT
value: "{{ $.Values.asteriskBridge.sftpPort }}"
- name: ASTERISK_SFTP_USERNAME
value: "{{ $.Values.asteriskBridge.sftpUsername }}"
- name: ASTERISK_SFTP_PASSWORD
value: "{{ $.Values.asteriskBridge.sftpPassword }}"
- name: ASTERISK_SFTP_BASE_PATH
value: "{{ $.Values.asteriskBridge.sftpBasePath }}"
- name: ASTERISK_BRIDGE_POLL_INTERVAL_SECONDS
value: "{{ $.Values.asteriskBridge.pollIntervalSeconds }}"
- name: ASTERISK_BRIDGE_AUTH_MODE
value: "{{ $.Values.asteriskBridge.authMode }}"
- name: ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY
value: "{{ $.Values.asteriskBridge.authFallbackLegacy }}"
- name: ASTERISK_BRIDGE_AUTH_SUBJECT
value: "{{ $.Values.asteriskBridge.authSubject }}"
- name: ASTERISK_BRIDGE_AUTH_USER
value: "{{ $.Values.asteriskBridge.authUser }}"
- name: ASTERISK_BRIDGE_AUTH_ROLE
value: "{{ $.Values.asteriskBridge.authRole }}"
- name: ASTERISK_BRIDGE_AUTH_TOKEN_TTL_SECONDS
value: "{{ $.Values.asteriskBridge.authTokenTtlSeconds }}"
- name: ASTERISK_IVR_FASTAGI_ENABLED
value: "{{ $.Values.asteriskBridge.ivrFastagiEnabled }}"
- name: ASTERISK_IVR_FASTAGI_HOST
value: "{{ $.Values.asteriskBridge.ivrFastagiHost }}"
- name: ASTERISK_IVR_FASTAGI_PORT
value: "{{ $.Values.asteriskBridge.ivrFastagiPort }}"
- name: ASTERISK_IVR_DTMF_TIMEOUT_SECONDS
value: "{{ $.Values.asteriskBridge.ivrDtmfTimeoutSeconds }}"
- name: ASTERISK_IVR_MAX_NO_INPUT_RETRIES
value: "{{ $.Values.asteriskBridge.ivrMaxNoInputRetries }}"
- name: ASTERISK_IVR_MAX_INVALID_RETRIES
value: "{{ $.Values.asteriskBridge.ivrMaxInvalidRetries }}"
- name: ASTERISK_IVR_CALL_LINK_WAIT_SECONDS
value: "{{ $.Values.asteriskBridge.ivrCallLinkWaitSeconds }}"
- name: ASTERISK_CALLCONTROL_ENABLED
value: "{{ $.Values.asteriskBridge.callcontrolEnabled }}"
- name: ASTERISK_CALLCONTROL_ACTION_TIMEOUT_SECONDS
value: "{{ $.Values.asteriskBridge.callcontrolActionTimeoutSeconds }}"
- name: ASTERISK_OPERATOR_EXTENSION_MAP_JSON
value: '{{ $.Values.asteriskBridge.operatorExtensionMapJson }}'
- name: ASTERISK_TRANSFER_TARGET_MAP_JSON
value: '{{ $.Values.asteriskBridge.transferTargetMapJson }}'
- name: ASTERISK_CALLCONTROL_CLAIM_CONTEXT
value: "{{ $.Values.asteriskBridge.claimContext }}"
- name: ASTERISK_CALLCONTROL_TRANSFER_CONTEXT
value: "{{ $.Values.asteriskBridge.transferContext }}"
- name: AI_VOICE_RUNTIME_SERVICE_URL
value: http://ai-voice-runtime-service:8000
- name: AI_VOICE_ENABLED
value: "{{ $.Values.voiceAI.enabled }}"
- name: AI_VOICE_QUEUE_CONFIG_JSON
value: '{{ $.Values.voiceAI.queueConfigJson }}'
- name: AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS
value: "{{ $.Values.voiceAI.runtimeTrustedServiceSubjects }}"
- name: INTERACTION_SERVICE_URL
value: http://interaction-service:8000
- name: VOICE_ADAPTER_SERVICE_URL
value: http://voice-adapter-service:8000
- name: RECORDING_SERVICE_URL
value: http://recording-service:8000
- name: IVR_SERVICE_URL
value: http://ivr-service:8000
{{- end }}
{{- if eq $name "ivr-service" }}
- name: IVR_RUNTIME_TRUSTED_SERVICE_SUBJECTS
value: "{{ $.Values.ivrRuntime.trustedServiceSubjects }}"
{{- end }}
{{- if eq $name "ai-orchestrator-service" }}
- name: INTERACTION_SERVICE_URL
value: http://interaction-service:8000
- name: TELEGRAM_ADAPTER_SERVICE_URL
value: http://telegram-adapter-service:8000
- name: AI_VOICE_MAX_CONTEXT_SEGMENTS
value: "{{ $.Values.voiceAI.maxContextSegments }}"
{{- end }}
{{- if eq $name "ai-voice-runtime-service" }}
- name: AI_ORCHESTRATOR_SERVICE_URL
value: http://ai-orchestrator-service:8000
- name: ASTERISK_BRIDGE_SERVICE_URL
value: http://asterisk-bridge-service:8000
- name: AI_VOICE_ASR_PROVIDER
value: "{{ $.Values.voiceAI.asrProvider }}"
- name: AI_VOICE_TTS_PROVIDER
value: "{{ $.Values.voiceAI.ttsProvider }}"
- name: AI_VOICE_AUDIOSOCKET_ENABLED
value: "{{ $.Values.voiceAI.audioSocketEnabled }}"
- name: AI_VOICE_AUDIOSOCKET_HOST
value: "{{ $.Values.voiceAI.audioSocketHost }}"
- name: AI_VOICE_AUDIOSOCKET_PORT
value: "{{ $.Values.voiceAI.audioSocketPort }}"
- name: AI_VOICE_ASR_MODEL
value: "{{ $.Values.voiceAI.asrModel }}"
- name: AI_VOICE_TTS_MODEL
value: "{{ $.Values.voiceAI.ttsModel }}"
- name: AI_VOICE_TTS_VOICE
value: "{{ $.Values.voiceAI.ttsVoice }}"
- name: AI_VOICE_TTS_CACHE_ENABLED
value: "{{ $.Values.voiceAI.ttsCacheEnabled }}"
- name: AI_VOICE_TTS_CACHE_DIR
value: "{{ $.Values.voiceAI.ttsCacheDir }}"
- name: AI_VOICE_VAD_MIN_SPEECH_MS
value: "{{ $.Values.voiceAI.vadMinSpeechMs }}"
- name: AI_VOICE_VAD_TRAILING_SILENCE_MS
value: "{{ $.Values.voiceAI.vadTrailingSilenceMs }}"
- name: AI_VOICE_TURN_MAX_MS
value: "{{ $.Values.voiceAI.turnMaxMs }}"
- name: AI_VOICE_MEDIA_IDLE_TIMEOUT_SECONDS
value: "{{ $.Values.voiceAI.mediaIdleTimeoutSeconds }}"
- name: AI_VOICE_HANDOFF_TIMEOUT_SECONDS
value: "{{ $.Values.voiceAI.handoffTimeoutSeconds }}"
{{- end }}
{{- if eq $name "recording-service" }}
- name: CC_RECORDINGS_DIR
value: /var/lib/mvp-cc/data/recordings
- name: RECORDING_MAX_BYTES
value: "26214400"
- name: RECORDING_IMPORT_TRUSTED_SERVICE_SUBJECTS
value: "{{ $.Values.recording.importTrustedServiceSubjects }}"
- name: RECORDING_IMPORT_ALLOW_ADMIN
value: "{{ $.Values.recording.importAllowAdmin }}"
{{- end }}
{{- if eq $name "voice-adapter-service" }}
- name: VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS
value: "{{ $.Values.voiceAdapter.trustedServiceSubjects }}"
{{- end }}
{{- if eq $name "auth-service" }}
- name: OIDC_ENABLED
value: "{{ $.Values.auth.oidc.enabled }}"
- name: OIDC_PROVIDER
value: "{{ $.Values.auth.oidc.provider }}"
- name: OIDC_ISSUER_URL
value: "{{ $.Values.auth.oidc.issuerUrl }}"
- name: OIDC_CLIENT_ID
value: "{{ $.Values.auth.oidc.clientId }}"
- name: OIDC_CLIENT_SECRET
value: "{{ $.Values.auth.oidc.clientSecret }}"
- name: OIDC_REDIRECT_URI
value: "{{ $.Values.auth.oidc.redirectUri }}"
- name: OIDC_SCOPES
value: "{{ $.Values.auth.oidc.scopes }}"
- name: OIDC_ROLE_CLAIM
value: "{{ $.Values.auth.oidc.roleClaim }}"
- name: OIDC_ROLE_MAP_JSON
value: '{{ $.Values.auth.oidc.roleMapJson }}'
{{- end }}
{{- if $isGateway }}
- name: AUTH_SERVICE_URL
value: http://auth-service:8000
- name: AUDIT_SERVICE_URL
value: http://audit-service:8000
- name: CUSTOMER_SERVICE_URL
value: http://customer-service:8000
- name: INTERACTION_SERVICE_URL
value: http://interaction-service:8000
- name: ROUTING_SERVICE_URL
value: http://routing-service:8000
- name: VOICE_ADAPTER_SERVICE_URL
value: http://voice-adapter-service:8000
- name: RECORDING_SERVICE_URL
value: http://recording-service:8000
- name: IVR_SERVICE_URL
value: http://ivr-service:8000
- name: EVENT_BUS_SERVICE_URL
value: http://event-bus-service:8000
- name: ASTERISK_BRIDGE_SERVICE_URL
value: http://asterisk-bridge-service:8000
- name: TELEGRAM_ADAPTER_SERVICE_URL
value: http://telegram-adapter-service:8000
- name: AI_ORCHESTRATOR_SERVICE_URL
value: http://ai-orchestrator-service:8000
- name: AI_VOICE_RUNTIME_SERVICE_URL
value: http://ai-voice-runtime-service:8000
- name: WEBCHAT_ADAPTER_SERVICE_URL
value: http://webchat-adapter-service:8000
- name: EMAIL_ADAPTER_SERVICE_URL
value: http://email-adapter-service:8000
- name: KB_SERVICE_URL
value: http://kb-service:8000
- name: REPORTING_SERVICE_URL
value: http://reporting-service:8000
- name: SUPERVISOR_SERVICE_URL
value: http://supervisor-service:8000
{{- end }}
ports:
- containerPort: 8000
{{- if eq $name "ai-voice-runtime-service" }}
- containerPort: 9019
{{- end }}
{{- if eq $name "asterisk-bridge-service" }}
- containerPort: 4573
{{- end }}
resources:
requests:
cpu: {{ $svc.resources.requests.cpu }}
memory: {{ $svc.resources.requests.memory }}
limits:
cpu: {{ $svc.resources.limits.cpu }}
memory: {{ $svc.resources.limits.memory }}
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 30
periodSeconds: 5
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 2
{{- if $svc.persistence.enabled }}
volumeMounts:
- name: app-data
mountPath: /var/lib/mvp-cc/data
{{- end }}
{{- if $svc.persistence.enabled }}
volumes:
- name: app-data
persistentVolumeClaim:
claimName: {{ $name }}-data
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Values.namespace }}
spec:
selector:
app: {{ $name }}
ports:
- name: http
port: 8000
targetPort: 8000
{{- if eq $name "ai-voice-runtime-service" }}
- name: audiosocket
port: 9019
targetPort: 9019
{{- end }}
{{- if eq $name "asterisk-bridge-service" }}
- name: fastagi
port: 4573
targetPort: 4573
{{- end }}
type: {{ if $isGateway }}LoadBalancer{{ else }}ClusterIP{{ end }}
---
{{- if $svc.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ $name }}-data
namespace: {{ $.Values.namespace }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ $svc.persistence.size }}
---
{{- end }}
{{- if and $svc.podDisruptionBudget.enabled (gt (int $svc.replicas) 1) }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $name }}-pdb
namespace: {{ $.Values.namespace }}
spec:
minAvailable: {{ $svc.podDisruptionBudget.minAvailable }}
selector:
matchLabels:
app: {{ $name }}
---
{{- end }}
{{- if $svc.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $name }}-hpa
namespace: {{ $.Values.namespace }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $name }}
minReplicas: {{ $svc.autoscaling.minReplicas }}
maxReplicas: {{ $svc.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ $svc.autoscaling.targetCPUUtilizationPercentage }}
---
{{- end }}
{{- end }}
+12
View File
@@ -0,0 +1,12 @@
profiles:
scale500:
enabled: true
auth:
allowLegacyHeaderAuth: "0"
db:
poolSize: "15"
maxOverflow: "30"
poolTimeoutSeconds: "30"
poolRecycleSeconds: "1800"
+77
View File
@@ -0,0 +1,77 @@
auth:
allowLegacyHeaderAuth: "0"
eventBus:
enabled: "0"
url: amqp://guest:guest@rabbitmq:5672/
exchange: mvpcc.domain.events
dispatchBatchSize: "50"
maxAttempts: "5"
pollIntervalSeconds: "2"
consumerEnabled: "1"
auditQueue: mvpcc.audit.events
reportingQueue: mvpcc.reporting.events
rabbitmq:
enabled: false
image: rabbitmq:3-management
replicas: 1
user: guest
password: guest
servicePort: 5672
managementPort: 15672
asteriskBridge:
enabled: "1"
amiEventPrefix: MVPCC
queueMapJson: '{"voice_lab":"<queue_id>"}'
authMode: bearer
authFallbackLegacy: "0"
authSubject: svc:asterisk-bridge
authUser: asterisk-bridge
authRole: admin
authTokenTtlSeconds: "300"
callcontrolEnabled: "0"
callcontrolActionTimeoutSeconds: "10"
operatorExtensionMapJson: "{}"
transferTargetMapJson: "{}"
claimContext: mvpcc-claim
transferContext: mvpcc-transfer
pollIntervalSeconds: "1"
voiceAdapter:
trustedServiceSubjects: svc:asterisk-bridge,svc:ivr-service
recording:
importTrustedServiceSubjects: svc:asterisk-bridge
importAllowAdmin: "0"
services:
auth-service:
replicas: 1
voice-adapter-service:
replicas: 1
recording-service:
replicas: 1
asterisk-bridge-service:
module: services.asterisk_bridge_service.app:app
# Must stay singleton: background AMI consumption and retry/reconcile loops are not multi-replica safe.
replicas: 1
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 1
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: false
minAvailable: 1
persistence:
enabled: false
size: 1Gi
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
+18
View File
@@ -0,0 +1,18 @@
auth:
allowLegacyHeaderAuth: "0"
asteriskBridge:
enabled: "1"
authMode: bearer
authFallbackLegacy: "0"
authSubject: svc:asterisk-bridge
authUser: asterisk-bridge
authRole: admin
authTokenTtlSeconds: "300"
voiceAdapter:
trustedServiceSubjects: svc:asterisk-bridge,svc:ivr-service
recording:
importTrustedServiceSubjects: svc:asterisk-bridge
importAllowAdmin: "0"
+538
View File
@@ -0,0 +1,538 @@
namespace: mvp-cc
image:
repository: mvp-cc-platform
tag: latest
global:
databaseUrl: postgresql://mvp:mvp@postgresql.mvp-cc.svc.cluster.local:5432/mvpcc
schemaManagementMode: migrations
runtimeClass: ""
imagePullPolicy: IfNotPresent
migrations:
enabled: true
backoffLimit: 1
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 600
hookDeletePolicy: before-hook-creation,hook-succeeded
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
profiles:
scale500:
enabled: false
db:
poolSize: "15"
maxOverflow: "30"
poolTimeoutSeconds: "30"
poolRecycleSeconds: "1800"
voiceAdapter:
trustedServiceSubjects: svc:asterisk-bridge,svc:ivr-service
ivrRuntime:
trustedServiceSubjects: svc:asterisk-bridge
recording:
importTrustedServiceSubjects: svc:asterisk-bridge
importAllowAdmin: "1"
voiceAI:
enabled: "0"
queueConfigJson: "{}"
asrProvider: openai
ttsProvider: openai
audioSocketEnabled: "1"
audioSocketHost: 0.0.0.0
audioSocketPort: "9019"
asrModel: gpt-4o-mini-transcribe
ttsModel: gpt-4o-mini-tts
ttsVoice: alloy
ttsCacheEnabled: "1"
ttsCacheDir: /var/lib/mvp-cc/data/ai_voice_tts_cache
vadMinSpeechMs: "300"
vadTrailingSilenceMs: "700"
turnMaxMs: "10000"
mediaIdleTimeoutSeconds: "15"
maxContextSegments: "8"
handoffTimeoutSeconds: "8"
runtimeTrustedServiceSubjects: svc:ai-voice-runtime
eventBus:
enabled: "0"
url: amqp://guest:guest@rabbitmq:5672/
exchange: mvpcc.domain.events
dispatchBatchSize: "50"
maxAttempts: "5"
pollIntervalSeconds: "2"
consumerEnabled: "1"
auditQueue: mvpcc.audit.events
reportingQueue: mvpcc.reporting.events
asteriskBridge:
enabled: "0"
amiHost: ""
amiPort: "5038"
amiUsername: ""
amiSecret: ""
amiEventPrefix: MVPCC
queueMapJson: "{}"
authMode: legacy_headers
authFallbackLegacy: "1"
authSubject: svc:asterisk-bridge
authUser: asterisk-bridge
authRole: admin
authTokenTtlSeconds: "300"
ivrFastagiEnabled: "0"
ivrFastagiHost: 0.0.0.0
ivrFastagiPort: "4573"
ivrDtmfTimeoutSeconds: "5"
ivrMaxNoInputRetries: "2"
ivrMaxInvalidRetries: "2"
ivrCallLinkWaitSeconds: "5"
callcontrolEnabled: "0"
callcontrolActionTimeoutSeconds: "10"
operatorExtensionMapJson: "{}"
transferTargetMapJson: "{}"
claimContext: mvpcc-claim
transferContext: mvpcc-transfer
sftpHost: ""
sftpPort: "22"
sftpUsername: ""
sftpPassword: ""
sftpBasePath: /var/spool/asterisk/monitor
pollIntervalSeconds: "1"
rabbitmq:
enabled: false
image: rabbitmq:3-management
replicas: 1
user: guest
password: guest
servicePort: 5672
managementPort: 15672
auth:
appTokenSecret: change-me-app-token-secret
appTokenTtlSeconds: "3600"
allowLegacyHeaderAuth: "1"
oidc:
enabled: "0"
provider: keycloak
issuerUrl: https://keycloak.example.local/realms/mvp-cc
clientId: mvp-cc
clientSecret: change-me-keycloak-client-secret
redirectUri: https://cc.example.local/proxy/auth/auth/oidc/callback
scopes: openid profile email
roleClaim: groups
roleMapJson: '{"kc_admin":"admin","kc_supervisor":"supervisor","kc_operator":"operator","kc_analyst":"analyst"}'
services:
auth-service:
module: services.auth_service.app:app
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 2
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
audit-service:
module: services.audit_service.app:app
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 2
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
customer-service:
module: services.customer_service.app:app
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 2
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
interaction-service:
module: services.interaction_service.app:app
replicas: 4
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1500m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 4
maxReplicas: 8
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
routing-service:
module: services.routing_service.app:app
replicas: 3
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1500m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 6
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
voice-adapter-service:
module: services.voice_adapter_service.app:app
replicas: 2
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
recording-service:
module: services.recording_service.app:app
replicas: 1
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 1
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: false
minAvailable: 1
persistence:
enabled: true
size: 10Gi
ivr-service:
module: services.ivr_service.app:app
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 2
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
event-bus-service:
module: services.event_bus_service.app:app
replicas: 1
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 1
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: false
minAvailable: 1
persistence:
enabled: false
size: 1Gi
asterisk-bridge-service:
module: services.asterisk_bridge_service.app:app
# Must stay singleton: background AMI consumption and retry/reconcile loops are not multi-replica safe.
replicas: 1
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 1
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: false
minAvailable: 1
persistence:
enabled: false
size: 1Gi
telegram-adapter-service:
module: services.telegram_adapter_service.app:app
replicas: 2
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
ai-orchestrator-service:
module: services.ai_orchestrator_service.app:app
replicas: 2
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
ai-voice-runtime-service:
module: services.ai_voice_runtime_service.app:app
replicas: 1
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 1
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: false
minAvailable: 1
persistence:
enabled: false
size: 1Gi
webchat-adapter-service:
module: services.webchat_adapter_service.app:app
replicas: 2
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
email-adapter-service:
module: services.email_adapter_service.app:app
replicas: 2
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
kb-service:
module: services.kb_service.app:app
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 2
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
reporting-service:
module: services.reporting_service.app:app
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 2
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
supervisor-service:
module: services.supervisor_service.app:app
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 750m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 2
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
api-gateway:
module: gateway.app:app
gateway: true
replicas: 4
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1500m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 4
maxReplicas: 8
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 1
persistence:
enabled: false
size: 1Gi
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: mvp-cc
data:
prometheus.yml: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'mvp-services'
metrics_path: /health
static_configs:
- targets:
- auth-service:8000
- audit-service:8000
- customer-service:8000
- interaction-service:8000
- routing-service:8000
- voice-adapter-service:8000
- telegram-adapter-service:8000
- kb-service:8000
- reporting-service:8000
- supervisor-service:8000
- api-gateway:8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: mvp-cc
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.55.1
args:
- --config.file=/etc/prometheus/prometheus.yml
volumeMounts:
- name: config
mountPath: /etc/prometheus
volumes:
- name: config
configMap:
name: prometheus-config
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: mvp-cc
spec:
selector:
app: prometheus
ports:
- name: http
port: 9090
targetPort: 9090
type: ClusterIP
+4
View File
@@ -0,0 +1,4 @@
track9/[0-9]*/
track9_1/[0-9]*/
track10/[0-9]*/
track11/[0-9]*/
+13
View File
@@ -0,0 +1,13 @@
# Track 10 Acceptance Evidence
Store Track 10 reliability evidence per run:
- `docs/acceptance/track10/<timestamp>/...`
Recommended contents:
- `voice-latency-latest.json`
- `command-output.txt`
- `track10-acceptance.md`
Reference runbook:
- `docs/runbooks/track10-live-voice-reliability.md`
@@ -0,0 +1,41 @@
# Track 10 Acceptance (Latest)
## Environment
- Date: `2026-03-05`
- Gateway URL: `http://127.0.0.1:8080`
- Database URL (masked): `sqlite:///.data_local/mvp_cc.db`
- Evidence folder: `docs/acceptance/track10/20260305_163330`
## Validation command
```powershell
python scripts\track10_voice_latency_report.py `
--database-url sqlite:///.data_local/mvp_cc.db `
--since-minutes 90 `
--breach-mode direct `
--json-out docs/acceptance/track10/20260305_163330/voice-latency-latest.json
```
## Result summary
- total calls: `10`
- active_no_end: `0`
- ended_no_recording_upload: `0`
- ended_direct_calls: `4`
- ended_reconciled_calls: `6`
- p95 started->ended: `1686.35s`
- p95 ended->recording upload: `455.25s`
- p95 direct started->ended: `33.0s`
- p95 direct ended->recording upload: `2.85s`
## Decision
- [x] GO (with exception)
- [ ] NO-GO
## Notes
- Direct path remains stable (`active_no_end=0`, `ended_no_recording_upload=0`).
- Threshold exception recorded: direct `started->ended` is `33s` vs target `30s`.
- Detailed run: `docs/acceptance/track10/20260305_163330/track10-acceptance.md`
+15
View File
@@ -0,0 +1,15 @@
# Track 11 Acceptance Evidence
Stable summary:
- [track11-acceptance.md](/e:/Zhan/docs/acceptance/track11/track11-acceptance.md)
Store Track 11 QA evidence per run:
- `docs/acceptance/track11/<timestamp>/track11-acceptance.md`
- `docs/acceptance/track11/<timestamp>/operator-live-calls.json`
- `docs/acceptance/track11/<timestamp>/bridge-actions.json`
- `docs/acceptance/track11/<timestamp>/playback-proof.json`
Reference runbook:
- `docs/runbooks/track11-live-operator-call-control.md`
@@ -0,0 +1,35 @@
# Track 11 Acceptance (Latest)
## Environment
- Date: `2026-03-07`
- Base URL: `http://localhost:8080`
- Database URL (masked): `sqlite:///.data_local/mvp_cc.db`
- Evidence package: [20260307_104748](/e:/Zhan/docs/acceptance/track11/20260307_104748)
## Executed scenario
1. Operator received a live inbound call in `/operator`.
2. Operator claimed the call.
3. Operator completed a real hangup path and recording playback was validated.
4. Operator completed a blind transfer flow from the live-call block.
## Validation summary
- live inbound visibility: `yes`
- claim persisted: `yes`
- hangup persisted: `yes`
- transfer persisted: `yes`
- `call.connected` present: `yes`
- `call.transferred` present: `yes`
- recording playback: `PASS`
## Result
- GO / NO-GO: `GO`
- Track 11 status: `Accepted`
## Notes
- Detailed package: `docs/acceptance/track11/20260307_104748/track11-acceptance.md`
- Track 11 keeps browser UI control and external softphone media as the accepted baseline.
@@ -0,0 +1,41 @@
# Track 11 Acceptance - <timestamp>
## Environment
- Base URL: `<gateway_base_url>`
- Database: `<database_url>`
- Asterisk VM: `<asterisk_host>`
- Operator account: `<operator_user>`
- Operator extension: `<operator_extension>`
## Executed scenario
1. Caller `1001` called `7000`.
2. Operator opened `/operator` -> `Живые звонки`.
3. Operator clicked `Принять в работу`.
4. Voice conversation in `MicroSIP/Zoiper` was confirmed.
5. Operator executed one or both actions:
- [ ] `Завершить`
- [ ] `Передать` (blind transfer)
6. Recording was checked in `/supervisor`.
## API and DB evidence
- Live call list saved: `operator-live-calls.json`
- Action log saved: `bridge-actions.json`
- Playback proof saved: `playback-proof.json`
## Mandatory checks
- [ ] `call.started` exists (`payload.source=asterisk`)
- [ ] `call.connected` exists
- [ ] `call.ended` exists
- [ ] `recording.ready` exists
- [ ] optional `call.transferred` exists when transfer path was tested
- [ ] no unresolved failed call-control actions in the acceptance window
## Decision
- GO / NO-GO: `<decision>`
- Notes:
- `<notes>`
@@ -0,0 +1,27 @@
{
"latest_call": "1772881979.108",
"latest_call_actions": [],
"reference_recent_ok_actions": [
{
"call_id": "1772881673.105",
"action_type": "hangup",
"actor_user": "operator",
"result_status": "ok",
"created_at": "2026-03-07T11:08:42+00:00"
},
{
"call_id": "1772881673.105",
"action_type": "claim",
"actor_user": "operator",
"result_status": "ok",
"created_at": "2026-03-07T11:08:16+00:00"
},
{
"call_id": "1772876380.93",
"action_type": "hangup",
"actor_user": "operator",
"result_status": "ok",
"created_at": "2026-03-07T09:40:02+00:00"
}
]
}
@@ -0,0 +1,25 @@
[
{
"call_id": "1772881979.108",
"caller_name": "1001",
"caller_number": "1001",
"channel_name": "PJSIP/2001-00000047",
"claimed_at": null,
"claimed_by_user": null,
"connected_at": "2026-03-07T11:13:05+00:00",
"ended_at": null,
"hangup_cause": "unknown",
"has_recording": false,
"interaction_id": "int_0349796d1e",
"last_transition_at": "2026-03-07T11:13:26+00:00",
"operator_extension": "2001",
"queue_code": "voice_lab",
"queue_id": "que_23fb0ad07c",
"started_at": "2026-03-07T11:13:01+00:00",
"status": "active",
"telephony_status": "connected",
"terminal_action": null,
"terminal_target": null,
"updated_at": "2026-03-07T11:13:26+00:00"
}
]
@@ -0,0 +1,10 @@
{
"call_id": "1772881979.108",
"interaction_id": "int_0349796d1e",
"recording_id": "rec_733ef323b1",
"url": "http://127.0.0.1:8080/proxy/recording/recordings/rec_733ef323b1/content?disposition=inline",
"status_code": 200,
"content_type": "audio/wav",
"content_length": 457324,
"file_name": "20260307-161300-1772881979.108.wav"
}
@@ -0,0 +1,75 @@
# Track 12 Acceptance - 20260307_161629
## Environment
- Base URL: `http://localhost:8080`
- Database: `sqlite:///./.data_local/mvp_cc.db`
- Asterisk host: `92.38.48.166`
- Operator user: `operator`
- Operator extension: `2001`
## Acceptance Basis
Track 12 is a UX/state hardening layer on top of accepted Track 11.
This package closes the Track 12 delta and keeps Track 11 control-path proof as the inherited baseline.
Reference control-path package:
- [track11-acceptance.md](/e:/Zhan/docs/acceptance/track11/20260307_104748/track11-acceptance.md)
Track 12 delta call:
- `call_id = 1772881979.108`
- `interaction_id = int_0349796d1e`
- `recording_id = rec_733ef323b1`
## Executed Scenarios
- [x] Happy path inherited from accepted Track 11 package
- [x] Transfer path inherited from accepted Track 11 package
- [x] Passive path: answer in SIP, no browser action, call remains in `live-calls` until real hangup
- [x] No-fake-cards path: `Живые звонки` uses bridge data only and does not show `interaction:int_*`
## Delta Evidence
- Active live call snapshot: [operator-live-calls.json](/e:/Zhan/docs/acceptance/track12/20260307_161629/operator-live-calls.json)
- Recent call snapshot: [operator-recent-calls.json](/e:/Zhan/docs/acceptance/track12/20260307_161629/operator-recent-calls.json)
- Empty live snapshot after call end: [operator-live-calls-empty.json](/e:/Zhan/docs/acceptance/track12/20260307_161629/operator-live-calls-empty.json)
- Action log summary: [bridge-actions.json](/e:/Zhan/docs/acceptance/track12/20260307_161629/bridge-actions.json)
- Playback proof: [playback-proof.json](/e:/Zhan/docs/acceptance/track12/20260307_161629/playback-proof.json)
- Live monitor trace: [track12-monitor.log](/e:/Zhan/docs/acceptance/track12/20260307_161629/track12-monitor.log)
## What The Monitor Proved
For call `1772881979.108`:
1. At `2026-03-07T11:13:01+00:00` the call entered `/asterisk/live-calls` as:
- `status = active`
- `telephony_status = ringing`
2. At `2026-03-07T11:13:05+00:00` it stayed in `/asterisk/live-calls` and moved to:
- `telephony_status = connected`
- `operator_extension = 2001`
3. At `2026-03-07T11:13:26+00:00` a synthetic reconciled `call.ended` was emitted, but the call still remained in `/asterisk/live-calls`.
4. Only at the real final hangup (`2026-03-07T11:13:30+00:00`) the call left `/asterisk/live-calls` and moved to `/asterisk/recent-calls`.
5. `recording.ready` was emitted and the recording was imported successfully.
This closes the earlier defect where the card could disappear from the operator flow on the reconciled service event instead of the real final hangup.
## Mandatory Checks
- [x] `GET /proxy/asterisk-bridge/asterisk/live-calls` returns only active calls
- [x] `GET /proxy/asterisk-bridge/asterisk/recent-calls` returns only recent ended calls
- [x] passive SIP answer does not force the card out of `Активные звонки` before real hangup
- [x] final hangup moves the card to `Только что завершённые`
- [x] no `interaction:int_*` fallback cards are used for `Живые звонки`
- [x] recording is available for the monitored call
- [x] Track 11 `claim / hangup / blind-transfer` control path remains covered by the accepted Track 11 package
## Notes
- The initial runtime regression after a local restart was caused by `scripts/local_stack.py` not loading `.env.production`; this is fixed and the launcher now auto-loads `.env.production`, `.env.local`, and `.env`.
- The current passive-path monitored call ended without browser actions, so `bridge-actions.json` contains no action rows for `1772881979.108`. This is expected for the passive-path check.
- Control-path actions (`claim`, `hangup`, `blind-transfer`) remain validated by the accepted Track 11 package and were not redefined by Track 12.
## Decision
- GO / NO-GO: `GO`
- Track 12 status: `Accepted`
@@ -0,0 +1,21 @@
[2026-03-07T16:10:31+05:00] monitor {"status": "started", "window_seconds": 180}
[2026-03-07T16:10:31+05:00] auth {"role": "operator", "status": "ok", "user": "operator"}
[2026-03-07T16:10:31+05:00] live-calls []
[2026-03-07T16:10:31+05:00] recent-calls [{"call_id": "1772881673.105", "caller_name": "1001", "caller_number": "1001", "channel_name": "PJSIP/2001-00000045", "claimed_at": "2026-03-07T11:08:26+00:00", "claimed_by_user": "operator", "connected_at": "2026-03-07T11:07:57+00:00", "ended_at": "2026-03-07T11:08:53+00:00", "hangup_cause": "16", "has_recording": true, "interaction_id": "int_052586cdaa", "last_transition_at": "2026-03-07T11:08:53+00:00", "operator_extension": "2001", "queue_code": "voice_lab", "queue_id": "que_23fb0ad07c", "started_at": "2026-03-07T11:07:55+00:00", "status": "ended", "telephony_status": "ended", "terminal_action": "hangup", "terminal_target": null, "updated_at": "2026-03-07T11:08:53+00:00"}]
[2026-03-07T16:10:31+05:00] voice-events [{"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_9245c8bb30", "event_type": "recording.ready", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "file_name": "20260307-160753-1772881673.105.wav", "linked_id": "1772881673.105", "mime_type": "audio/wav", "remote_path": "/var/spool/asterisk/monitor/mvpcc/20260307-160753-1772881673.105.wav", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_3e2c4eedc7", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "hangup_cause": "16", "linked_id": "1772881673.105", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:20+00:00", "event_id": "vev_dbf1ba4399", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": null, "hangup_cause": "unknown", "linked_id": "1772881673.105", "reconciled": true, "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:07:57+00:00", "event_id": "vev_8be5ab34b9", "event_type": "call.connected", "interaction_id": "int_052586cdaa", "payload": {"channel_name": "PJSIP/2001-00000045", "claimed_by_user": null, "linked_id": "1772881673.105", "operator_extension": "2001", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:07:55+00:00", "event_id": "vev_2b41d093c7", "event_type": "call.started", "interaction_id": "int_052586cdaa", "payload": {"caller_name": "1001", "caller_number": "1001", "context": "from-softphones", "direction": "inbound", "extension": "7000", "linked_id": "1772881673.105", "source": "asterisk"}}, {"call_id": "1772876380.93", "created_at": "2026-03-07T09:40:12+00:00", "event_id": "vev_4c44dd5888", "event_type": "recording.ready", "interaction_id": "int_69443612e1", "payload": {"duration_seconds": 31, "file_name": "20260307-143940-1772876380.93.wav", "linked_id": "1772876380.93", "mime_type": "audio/wav", "remote_path": "/var/spool/asterisk/monitor/mvpcc/20260307-143940-1772876380.93.wav", "source": "asterisk"}}]
[2026-03-07T16:10:31+05:00] call-links [{"call_id": "1772881673.105", "claimed_by_user": "operator", "connected_at": "2026-03-07T11:07:57+00:00", "ended_at": "2026-03-07T11:08:53+00:00", "interaction_id": "int_052586cdaa", "operator_extension": "2001", "started_at": "2026-03-07T11:07:55+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772876380.93", "claimed_by_user": null, "connected_at": "2026-03-07T09:39:49+00:00", "ended_at": "2026-03-07T09:40:12+00:00", "interaction_id": "int_69443612e1", "operator_extension": "2001", "started_at": "2026-03-07T09:39:42+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772875171.90", "claimed_by_user": null, "connected_at": "2026-03-07T09:19:36+00:00", "ended_at": "2026-03-07T09:19:39+00:00", "interaction_id": "int_726c36d95d", "operator_extension": "2001", "started_at": "2026-03-07T09:19:33+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772874280.87", "claimed_by_user": null, "connected_at": "2026-03-07T09:04:47+00:00", "ended_at": "2026-03-07T09:04:54+00:00", "interaction_id": "int_6ef8abe6ec", "operator_extension": "2001", "started_at": "2026-03-07T09:04:42+00:00", "status": "ended", "telephony_status": "ended"}]
[2026-03-07T16:10:53+05:00] recent-calls []
[2026-03-07T16:13:01+05:00] live-calls [{"call_id": "1772881979.108", "caller_name": "1001", "caller_number": "1001", "channel_name": "PJSIP/1001-00000046", "claimed_at": null, "claimed_by_user": null, "connected_at": null, "ended_at": null, "hangup_cause": null, "has_recording": false, "interaction_id": "int_0349796d1e", "last_transition_at": "2026-03-07T11:13:01+00:00", "operator_extension": null, "queue_code": "voice_lab", "queue_id": "que_23fb0ad07c", "started_at": "2026-03-07T11:13:01+00:00", "status": "active", "telephony_status": "ringing", "terminal_action": null, "terminal_target": null, "updated_at": "2026-03-07T11:13:01+00:00"}]
[2026-03-07T16:13:01+05:00] voice-events [{"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:01+00:00", "event_id": "vev_8ece03ec07", "event_type": "call.started", "interaction_id": "int_0349796d1e", "payload": {"caller_name": "1001", "caller_number": "1001", "context": "from-softphones", "direction": "inbound", "extension": "7000", "linked_id": "1772881979.108", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_9245c8bb30", "event_type": "recording.ready", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "file_name": "20260307-160753-1772881673.105.wav", "linked_id": "1772881673.105", "mime_type": "audio/wav", "remote_path": "/var/spool/asterisk/monitor/mvpcc/20260307-160753-1772881673.105.wav", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_3e2c4eedc7", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "hangup_cause": "16", "linked_id": "1772881673.105", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:20+00:00", "event_id": "vev_dbf1ba4399", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": null, "hangup_cause": "unknown", "linked_id": "1772881673.105", "reconciled": true, "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:07:57+00:00", "event_id": "vev_8be5ab34b9", "event_type": "call.connected", "interaction_id": "int_052586cdaa", "payload": {"channel_name": "PJSIP/2001-00000045", "claimed_by_user": null, "linked_id": "1772881673.105", "operator_extension": "2001", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:07:55+00:00", "event_id": "vev_2b41d093c7", "event_type": "call.started", "interaction_id": "int_052586cdaa", "payload": {"caller_name": "1001", "caller_number": "1001", "context": "from-softphones", "direction": "inbound", "extension": "7000", "linked_id": "1772881673.105", "source": "asterisk"}}]
[2026-03-07T16:13:01+05:00] call-links [{"call_id": "1772881979.108", "claimed_by_user": null, "connected_at": null, "ended_at": null, "interaction_id": "int_0349796d1e", "operator_extension": null, "started_at": "2026-03-07T11:13:01+00:00", "status": "active", "telephony_status": "ringing"}, {"call_id": "1772881673.105", "claimed_by_user": "operator", "connected_at": "2026-03-07T11:07:57+00:00", "ended_at": "2026-03-07T11:08:53+00:00", "interaction_id": "int_052586cdaa", "operator_extension": "2001", "started_at": "2026-03-07T11:07:55+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772876380.93", "claimed_by_user": null, "connected_at": "2026-03-07T09:39:49+00:00", "ended_at": "2026-03-07T09:40:12+00:00", "interaction_id": "int_69443612e1", "operator_extension": "2001", "started_at": "2026-03-07T09:39:42+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772875171.90", "claimed_by_user": null, "connected_at": "2026-03-07T09:19:36+00:00", "ended_at": "2026-03-07T09:19:39+00:00", "interaction_id": "int_726c36d95d", "operator_extension": "2001", "started_at": "2026-03-07T09:19:33+00:00", "status": "ended", "telephony_status": "ended"}]
[2026-03-07T16:13:06+05:00] live-calls [{"call_id": "1772881979.108", "caller_name": "1001", "caller_number": "1001", "channel_name": "PJSIP/2001-00000047", "claimed_at": null, "claimed_by_user": null, "connected_at": "2026-03-07T11:13:05+00:00", "ended_at": null, "hangup_cause": null, "has_recording": false, "interaction_id": "int_0349796d1e", "last_transition_at": "2026-03-07T11:13:05+00:00", "operator_extension": "2001", "queue_code": "voice_lab", "queue_id": "que_23fb0ad07c", "started_at": "2026-03-07T11:13:01+00:00", "status": "active", "telephony_status": "connected", "terminal_action": null, "terminal_target": null, "updated_at": "2026-03-07T11:13:05+00:00"}]
[2026-03-07T16:13:06+05:00] voice-events [{"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:05+00:00", "event_id": "vev_e3bec0b0e6", "event_type": "call.connected", "interaction_id": "int_0349796d1e", "payload": {"channel_name": "PJSIP/2001-00000047", "claimed_by_user": null, "linked_id": "1772881979.108", "operator_extension": "2001", "source": "asterisk"}}, {"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:01+00:00", "event_id": "vev_8ece03ec07", "event_type": "call.started", "interaction_id": "int_0349796d1e", "payload": {"caller_name": "1001", "caller_number": "1001", "context": "from-softphones", "direction": "inbound", "extension": "7000", "linked_id": "1772881979.108", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_9245c8bb30", "event_type": "recording.ready", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "file_name": "20260307-160753-1772881673.105.wav", "linked_id": "1772881673.105", "mime_type": "audio/wav", "remote_path": "/var/spool/asterisk/monitor/mvpcc/20260307-160753-1772881673.105.wav", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_3e2c4eedc7", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "hangup_cause": "16", "linked_id": "1772881673.105", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:20+00:00", "event_id": "vev_dbf1ba4399", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": null, "hangup_cause": "unknown", "linked_id": "1772881673.105", "reconciled": true, "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:07:57+00:00", "event_id": "vev_8be5ab34b9", "event_type": "call.connected", "interaction_id": "int_052586cdaa", "payload": {"channel_name": "PJSIP/2001-00000045", "claimed_by_user": null, "linked_id": "1772881673.105", "operator_extension": "2001", "source": "asterisk"}}]
[2026-03-07T16:13:06+05:00] call-links [{"call_id": "1772881979.108", "claimed_by_user": null, "connected_at": "2026-03-07T11:13:05+00:00", "ended_at": null, "interaction_id": "int_0349796d1e", "operator_extension": "2001", "started_at": "2026-03-07T11:13:01+00:00", "status": "active", "telephony_status": "connected"}, {"call_id": "1772881673.105", "claimed_by_user": "operator", "connected_at": "2026-03-07T11:07:57+00:00", "ended_at": "2026-03-07T11:08:53+00:00", "interaction_id": "int_052586cdaa", "operator_extension": "2001", "started_at": "2026-03-07T11:07:55+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772876380.93", "claimed_by_user": null, "connected_at": "2026-03-07T09:39:49+00:00", "ended_at": "2026-03-07T09:40:12+00:00", "interaction_id": "int_69443612e1", "operator_extension": "2001", "started_at": "2026-03-07T09:39:42+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772875171.90", "claimed_by_user": null, "connected_at": "2026-03-07T09:19:36+00:00", "ended_at": "2026-03-07T09:19:39+00:00", "interaction_id": "int_726c36d95d", "operator_extension": "2001", "started_at": "2026-03-07T09:19:33+00:00", "status": "ended", "telephony_status": "ended"}]
[2026-03-07T16:13:27+05:00] live-calls [{"call_id": "1772881979.108", "caller_name": "1001", "caller_number": "1001", "channel_name": "PJSIP/2001-00000047", "claimed_at": null, "claimed_by_user": null, "connected_at": "2026-03-07T11:13:05+00:00", "ended_at": null, "hangup_cause": "unknown", "has_recording": false, "interaction_id": "int_0349796d1e", "last_transition_at": "2026-03-07T11:13:26+00:00", "operator_extension": "2001", "queue_code": "voice_lab", "queue_id": "que_23fb0ad07c", "started_at": "2026-03-07T11:13:01+00:00", "status": "active", "telephony_status": "connected", "terminal_action": null, "terminal_target": null, "updated_at": "2026-03-07T11:13:26+00:00"}]
[2026-03-07T16:13:27+05:00] voice-events [{"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:26+00:00", "event_id": "vev_9a03fc9ff8", "event_type": "call.ended", "interaction_id": "int_0349796d1e", "payload": {"duration_seconds": null, "hangup_cause": "unknown", "linked_id": "1772881979.108", "reconciled": true, "source": "asterisk"}}, {"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:05+00:00", "event_id": "vev_e3bec0b0e6", "event_type": "call.connected", "interaction_id": "int_0349796d1e", "payload": {"channel_name": "PJSIP/2001-00000047", "claimed_by_user": null, "linked_id": "1772881979.108", "operator_extension": "2001", "source": "asterisk"}}, {"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:01+00:00", "event_id": "vev_8ece03ec07", "event_type": "call.started", "interaction_id": "int_0349796d1e", "payload": {"caller_name": "1001", "caller_number": "1001", "context": "from-softphones", "direction": "inbound", "extension": "7000", "linked_id": "1772881979.108", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_9245c8bb30", "event_type": "recording.ready", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "file_name": "20260307-160753-1772881673.105.wav", "linked_id": "1772881673.105", "mime_type": "audio/wav", "remote_path": "/var/spool/asterisk/monitor/mvpcc/20260307-160753-1772881673.105.wav", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_3e2c4eedc7", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "hangup_cause": "16", "linked_id": "1772881673.105", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:20+00:00", "event_id": "vev_dbf1ba4399", "event_type": "call.ended", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": null, "hangup_cause": "unknown", "linked_id": "1772881673.105", "reconciled": true, "source": "asterisk"}}]
[2026-03-07T16:13:30+05:00] live-calls []
[2026-03-07T16:13:30+05:00] recent-calls [{"call_id": "1772881979.108", "caller_name": "1001", "caller_number": "1001", "channel_name": "PJSIP/2001-00000047", "claimed_at": null, "claimed_by_user": null, "connected_at": "2026-03-07T11:13:05+00:00", "ended_at": "2026-03-07T11:13:30+00:00", "hangup_cause": "16", "has_recording": false, "interaction_id": "int_0349796d1e", "last_transition_at": "2026-03-07T11:13:30+00:00", "operator_extension": "2001", "queue_code": "voice_lab", "queue_id": "que_23fb0ad07c", "started_at": "2026-03-07T11:13:01+00:00", "status": "ended", "telephony_status": "ended", "terminal_action": "ended", "terminal_target": null, "updated_at": "2026-03-07T11:13:30+00:00"}]
[2026-03-07T16:13:30+05:00] voice-events [{"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:30+00:00", "event_id": "vev_42cd34ba3b", "event_type": "recording.ready", "interaction_id": "int_0349796d1e", "payload": {"duration_seconds": 28, "file_name": "20260307-161300-1772881979.108.wav", "linked_id": "1772881979.108", "mime_type": "audio/wav", "remote_path": "/var/spool/asterisk/monitor/mvpcc/20260307-161300-1772881979.108.wav", "source": "asterisk"}}, {"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:30+00:00", "event_id": "vev_dcf3378912", "event_type": "call.ended", "interaction_id": "int_0349796d1e", "payload": {"duration_seconds": 28, "hangup_cause": "16", "linked_id": "1772881979.108", "source": "asterisk"}}, {"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:26+00:00", "event_id": "vev_9a03fc9ff8", "event_type": "call.ended", "interaction_id": "int_0349796d1e", "payload": {"duration_seconds": null, "hangup_cause": "unknown", "linked_id": "1772881979.108", "reconciled": true, "source": "asterisk"}}, {"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:05+00:00", "event_id": "vev_e3bec0b0e6", "event_type": "call.connected", "interaction_id": "int_0349796d1e", "payload": {"channel_name": "PJSIP/2001-00000047", "claimed_by_user": null, "linked_id": "1772881979.108", "operator_extension": "2001", "source": "asterisk"}}, {"call_id": "1772881979.108", "created_at": "2026-03-07T11:13:01+00:00", "event_id": "vev_8ece03ec07", "event_type": "call.started", "interaction_id": "int_0349796d1e", "payload": {"caller_name": "1001", "caller_number": "1001", "context": "from-softphones", "direction": "inbound", "extension": "7000", "linked_id": "1772881979.108", "source": "asterisk"}}, {"call_id": "1772881673.105", "created_at": "2026-03-07T11:08:53+00:00", "event_id": "vev_9245c8bb30", "event_type": "recording.ready", "interaction_id": "int_052586cdaa", "payload": {"duration_seconds": 58, "file_name": "20260307-160753-1772881673.105.wav", "linked_id": "1772881673.105", "mime_type": "audio/wav", "remote_path": "/var/spool/asterisk/monitor/mvpcc/20260307-160753-1772881673.105.wav", "source": "asterisk"}}]
[2026-03-07T16:13:30+05:00] call-links [{"call_id": "1772881979.108", "claimed_by_user": null, "connected_at": "2026-03-07T11:13:05+00:00", "ended_at": "2026-03-07T11:13:30+00:00", "interaction_id": "int_0349796d1e", "operator_extension": "2001", "started_at": "2026-03-07T11:13:01+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772881673.105", "claimed_by_user": "operator", "connected_at": "2026-03-07T11:07:57+00:00", "ended_at": "2026-03-07T11:08:53+00:00", "interaction_id": "int_052586cdaa", "operator_extension": "2001", "started_at": "2026-03-07T11:07:55+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772876380.93", "claimed_by_user": null, "connected_at": "2026-03-07T09:39:49+00:00", "ended_at": "2026-03-07T09:40:12+00:00", "interaction_id": "int_69443612e1", "operator_extension": "2001", "started_at": "2026-03-07T09:39:42+00:00", "status": "ended", "telephony_status": "ended"}, {"call_id": "1772875171.90", "claimed_by_user": null, "connected_at": "2026-03-07T09:19:36+00:00", "ended_at": "2026-03-07T09:19:39+00:00", "interaction_id": "int_726c36d95d", "operator_extension": "2001", "started_at": "2026-03-07T09:19:33+00:00", "status": "ended", "telephony_status": "ended"}]
[2026-03-07T16:13:31+05:00] recent-calls [{"call_id": "1772881979.108", "caller_name": "1001", "caller_number": "1001", "channel_name": "PJSIP/2001-00000047", "claimed_at": null, "claimed_by_user": null, "connected_at": "2026-03-07T11:13:05+00:00", "ended_at": "2026-03-07T11:13:30+00:00", "hangup_cause": "16", "has_recording": true, "interaction_id": "int_0349796d1e", "last_transition_at": "2026-03-07T11:13:30+00:00", "operator_extension": "2001", "queue_code": "voice_lab", "queue_id": "que_23fb0ad07c", "started_at": "2026-03-07T11:13:01+00:00", "status": "ended", "telephony_status": "ended", "terminal_action": "ended", "terminal_target": null, "updated_at": "2026-03-07T11:13:30+00:00"}]
[2026-03-07T16:13:32+05:00] monitor {"status": "finished"}
@@ -0,0 +1,38 @@
# Track 12 Acceptance - <timestamp>
## Environment
- Base URL: `<gateway_base_url>`
- Database: `<database_url>`
- Asterisk host: `<asterisk_host>`
- Operator user: `<operator_user>`
- Operator extension: `<operator_extension>`
## Executed scenarios
- [ ] Happy path: answer in SIP -> `Принять в работу` -> `Завершить`
- [ ] Transfer path: answer in SIP -> `Принять в работу` -> `Передать`
- [ ] Passive path: answer in SIP, no browser action, call stays active until real hangup
- [ ] No-fake-cards path: `Живые звонки` does not show `interaction:int_*`
## Evidence
- Active live call snapshot: `operator-live-calls.json`
- Recent call snapshot: `operator-recent-calls.json`
- Action log: `bridge-actions.json`
- Playback proof: `playback-proof.json`
## Mandatory checks
- [ ] `GET /proxy/asterisk-bridge/asterisk/live-calls` returns only active calls
- [ ] `GET /proxy/asterisk-bridge/asterisk/recent-calls` returns only recent ended/transferred calls
- [ ] `claim` keeps the card in `Активные звонки`
- [ ] `hangup` moves the card to `Только что завершённые`
- [ ] `blind transfer` moves the card to `Только что завершённые`
- [ ] recording is available in `/supervisor`
## Decision
- GO / NO-GO: `<decision>`
- Notes:
- `<notes>`
@@ -0,0 +1,38 @@
# Track 14 Acceptance
## Environment
- Browser: `<chrome|edge version>`
- Gateway base URL: `<url>`
- Asterisk host: `<host>`
- WSS URL: `<wss-url>`
- Operator app user: `<user>`
- Browser SIP auth user: `<sip-username>`
## Scenario
1. Open `/operator` in Chrome or Edge.
2. Connect browser softphone.
3. Place one inbound call `1001 -> 7000`.
4. Answer in the browser.
5. Confirm auto-claim or perform manual `Принять в работу`.
6. Hang up through the browser path.
7. Confirm the call moves to recent and the recording is playable in `/supervisor`.
## Evidence
- Browser softphone registration state screenshot
- Operator live-call screenshot while ringing or connected
- Browser answer or hangup action proof
- Bridge actions snapshot
- Recent-calls snapshot
- Recording playback proof
## Result
- `GO` or `NO-GO`
- Notes:
- browser registration
- answer behavior
- auto-claim result
- recording result
@@ -0,0 +1,41 @@
# Track 15 Acceptance
## Environment
- Base URL:
- Telegram bot username:
- Queue ID:
- Operator user:
- Date/time:
## Scenario
1. Send the first Telegram message to the bot.
2. Open `/operator` -> `Telegram`.
3. Confirm a new thread appears and links to a Telegram interaction.
4. Claim the thread.
5. Send a reply from the operator page.
6. Send another inbound message from the same Telegram chat.
7. Confirm the same thread and same interaction are reused.
8. Close the thread.
9. Send one more inbound message and confirm reactivation to `new`.
## Expected results
- thread created on first inbound
- same thread reused on repeated inbound from the same `chat_id`
- operator reply persisted and delivered
- closed interaction reactivated on new inbound
- unsupported content, if tested, is shown as a system placeholder
## Evidence
- thread payload:
- messages payload:
- linked interaction payload:
- reply proof:
- reactivation proof:
## Result
- PASS / FAIL
+27
View File
@@ -0,0 +1,27 @@
# Track 9 Acceptance Evidence
Use this folder for formal QA-lab acceptance artifacts for Asterisk integration.
Recommended structure:
- `docs/acceptance/track9/<timestamp>/...` for each run
- include:
- bridge status snapshots
- smoke/check outputs
- sampled DB evidence
- final acceptance note
Generate a baseline package with:
```powershell
python scripts\track9_collect_evidence.py --base-url http://localhost:8080 --database-url <database-url> --run-checks --require-recording
```
When `--run-checks` is used, the package now includes:
- `preflight_output.txt`
- `smoke_output.txt`
- `track9_check_output.txt`
For production cutover tracking (Track 9.2), start from:
- [track9-cutover-sheet.template.md](/e:/Zhan/docs/acceptance/track9/track9-cutover-sheet.template.md)
@@ -0,0 +1,50 @@
# Track 9 Acceptance (Latest QA Run)
## Environment
- Date: `2026-03-05`
- Host: local QA lab (`127.0.0.1`)
- Gateway URL: `http://127.0.0.1:8080`
- Asterisk VM host: `92.38.48.166`
- Database URL (masked): `sqlite:///.data_local/mvp_cc.db`
- Evidence package: [20260305_160002](/e:/Zhan/docs/acceptance/track9/20260305_160002)
## Executed scenario
1. Softphone `1001` called `7000`.
2. Bridge reported AMI connected.
3. `call.started` received from Asterisk source.
4. `call.ended` received.
5. `recording.ready` processed and recording uploaded.
6. Recording playback validated in supervisor shell.
## Validation commands
```powershell
python scripts\track9_preflight.py --base-url <gateway> --check-sftp
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
```
## Result summary
- AMI connected: `yes`
- call.started present: `yes`
- call.ended present: `yes`
- call links present: `yes`
- recording imported: `yes`
- failed bridge events: `0`
- smoke: `PASS`
- track9_check: `PASS`
- preflight: `PASS`
## Risks / open items
- Asterisk dialplan still emits some duplicate/late lifecycle signals in lab runs; bridge reconciliation masks this and keeps platform data consistent.
- For production cutover, keep strict auth and rollback window from Track 9.2 sheet.
## Decision
- [x] GO
- [ ] NO-GO
@@ -0,0 +1,87 @@
# Track 9.2 Cutover Sheet (Template)
## Window
- Date:
- Start time:
- End time:
- Change ticket:
## Owners
- K8s owner:
- Voice/Asterisk owner:
- Application owner:
- On-call:
## Environment contract
- `KUBE_CONTEXT`:
- `NAMESPACE`:
- `RELEASE`:
- `GATEWAY_BASE_URL`:
- `DATABASE_URL`:
- `IMAGE_TAG`:
- `ASTERISK_AMI_HOST`:
- `ASTERISK_SFTP_HOST`:
- `QUEUE_ID (voice_lab)`:
## Entry checks (T-1 day)
- [ ] QA baseline accepted:
- `docs/acceptance/track9/track9-acceptance.md`
- [ ] Production queue map validated (`voice_lab -> queue_id` exists)
- [ ] AMI port reachable (`5038/tcp`)
- [ ] SFTP port reachable (`22/tcp`)
- [ ] PostgreSQL confirmed for target env
## Dry-run results (T-4h to T-1h)
- [ ] `helm lint` PASS
- [ ] `helm template` PASS
- [ ] strict env keys found in rendered manifest
- Artifact folder:
## Execution (T0)
- Helm command:
- Start time:
- End time:
- Result:
Rollout statuses:
- [ ] `api-gateway` ready
- [ ] `asterisk-bridge-service` ready
- [ ] `voice-adapter-service` ready
- [ ] `recording-service` ready
## Post-cutover checks
- [ ] `track9_preflight --check-sftp --require-strict-service-auth` PASS
- [ ] Real call completed (`1001 -> 7000`)
- [ ] `asterisk_lab_smoke --require-recording` PASS
- [ ] `track9_check --require-recording` PASS
- [ ] Supervisor playback confirmed (`HTTP 200`, `audio/*`)
## Evidence
- Evidence folder:
- `track9-acceptance.md`:
- `playback-proof.md`:
- `playback-proof.json` (if used):
## Decision
- [ ] GO
- [ ] NO-GO
Reason:
## Rollback section (fill if NO-GO)
- Trigger:
- Rollback command:
- Rollback completion time:
- Post-rollback preflight result:
- Incident / RCA ticket:
+18
View File
@@ -0,0 +1,18 @@
# Track 9.1 Acceptance (Latest Strict Auth Baseline)
## Summary
Track 9.1 captures the strict service-token hardening pass for the Asterisk bridge flow.
- strict auth profile: `ALLOW_LEGACY_HEADER_AUTH=0`
- bridge auth mode: `ASTERISK_BRIDGE_AUTH_MODE=bearer`
- trusted service subjects enforced for voice ingest and recording import
## Latest retained evidence package
- [20260305_042801](/e:/Zhan/docs/acceptance/track9_1/20260305_042801)
## Notes
- Earlier timestamp bundles were reduced during repository cleanup.
- Use the retained package for low-level QA artifacts and Track 9.1 reference data.
+91
View File
@@ -0,0 +1,91 @@
# 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
@@ -0,0 +1,59 @@
# 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`.
@@ -0,0 +1,44 @@
{
"$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
@@ -0,0 +1,30 @@
{
"$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
}
+37
View File
@@ -0,0 +1,37 @@
# Event Schema Index
- `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`
Each file is a JSON Schema (draft 2020-12) and can be used as a validation contract between services.
Track 8 introduces a canonical transport envelope for bus-delivered events:
- `docs/architecture/event-envelope.json`
Payload-specific schemas under `contracts/events` remain the authoritative contracts for the
domain payload inside that envelope.
Track 4 now provides the concrete schema file for the recording import placeholder:
- `docs/architecture/call.recording.ready.json`
Track 5 adds the concrete schema for IVR completion:
- `docs/architecture/ivr.completed.json`
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)
+54
View File
@@ -0,0 +1,54 @@
{
"$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
}
+25
View File
@@ -0,0 +1,25 @@
# Architecture Overview
## 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
## 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
## Deployment
- Local: Docker Compose
- On-prem: Kubernetes manifests and Helm chart
+744
View File
@@ -0,0 +1,744 @@
# 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.
+8
View File
@@ -0,0 +1,8 @@
# 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
@@ -0,0 +1,8 @@
# 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
@@ -0,0 +1,12 @@
# 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
@@ -0,0 +1,35 @@
# 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
@@ -0,0 +1,71 @@
# 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
@@ -0,0 +1,16 @@
# 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
@@ -0,0 +1,12 @@
# 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
@@ -0,0 +1,37 @@
# 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
@@ -0,0 +1,22 @@
# 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
@@ -0,0 +1,25 @@
# 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
@@ -0,0 +1,21 @@
# 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
@@ -0,0 +1,20 @@
# 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
@@ -0,0 +1,22 @@
# 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
@@ -0,0 +1,126 @@
# 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).
@@ -0,0 +1,65 @@
# 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`.
@@ -0,0 +1,60 @@
# 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.
@@ -0,0 +1,57 @@
# 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.
@@ -0,0 +1,45 @@
# 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
@@ -0,0 +1,30 @@
# 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
@@ -0,0 +1,11 @@
# 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
@@ -0,0 +1,132 @@
# `.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.

Some files were not shown because too many files have changed in this diff Show More