Initial import with GitLab CI/CD and registry deploy flow
This commit is contained in:
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"]
|
||||
@@ -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`.
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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 } } }
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
APP_IMAGE=registry.example.com/group/project/app:latest
|
||||
ASTERISK_IMAGE=registry.example.com/group/project/asterisk:latest
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .Values.namespace }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,12 @@
|
||||
profiles:
|
||||
scale500:
|
||||
enabled: true
|
||||
|
||||
auth:
|
||||
allowLegacyHeaderAuth: "0"
|
||||
|
||||
db:
|
||||
poolSize: "15"
|
||||
maxOverflow: "30"
|
||||
poolTimeoutSeconds: "30"
|
||||
poolRecycleSeconds: "1800"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
track9/[0-9]*/
|
||||
track9_1/[0-9]*/
|
||||
track10/[0-9]*/
|
||||
track11/[0-9]*/
|
||||
@@ -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`
|
||||
@@ -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 @@
|
||||
[]
|
||||
@@ -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 @@
|
||||
[]
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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,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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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/`
|
||||
@@ -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`)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user