Initial import with GitLab CI/CD and registry deploy flow

This commit is contained in:
Yera All
2026-04-02 17:05:45 +05:00
commit 5374c202d9
338 changed files with 81297 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
# API Matrix by Stage
## Stage 1
- auth-service: `/auth/login`, `/auth/me`, `/users`
- audit-service: `/audit/events`
- all services: `/health`
## Stage 2
- customer-service: `/customers`
- interaction-service: `/interactions`
- routing-service: `/queues`
- voice-adapter-service: `/integrations/voice/events`
- telegram-adapter-service: `/integrations/telegram/webhook`
## Stage 3
- kb-service: `/knowledge/*`
- reporting-service: `/reports/kpi`, `/reports/export`
- supervisor-service: `/supervisor/realtime`
## Stage 4
- recording-service: `/recordings`, `/recordings/*`
## Stage 5
- ivr-service: `/ivr/flows`, `/ivr/flows/*`, `/ivr/sessions`, `/ivr/sessions/*`
- routing-service: `/queues/{queue_id}/route` with optional `ivr_session_id`
## Stage 6
- reporting-service: `/reports/kpi` with optional `channel`
- reporting-service: `/reports/coverage`
- reporting-service: `/reports/export` with optional `queue_id` and `channel`
## Stage 7
- no new business REST routes
- ops CLI:
- `scripts/load_test.py` with profile-driven mixed workload
- `scripts/track7_check.py` for scale/hardening evidence validation
## Stage 8
- no new business REST routes
- event-bus-service (ops only): `/bus/outbox`, `/bus/outbox/*`
- ops CLI:
- `scripts/event_bus_smoke.py`
- `scripts/track8_check.py`
## Stage 9
- no new business REST routes
- recording-service: `/recordings/import-upload` (internal bridge upload)
- asterisk-bridge-service (ops only): `/asterisk/status`, `/asterisk/events`, `/asterisk/events/*`
- bridge-to-platform auth modes:
- `legacy_headers` (QA baseline)
- `bearer_first` / `bearer` (Track 9.1 hardening)
- ops CLI:
- `scripts/track9_preflight.py`
- `scripts/asterisk_lab_smoke.py`
- `scripts/track9_check.py`
- `scripts/track9_collect_evidence.py`
- `scripts/track9_2_cutover.ps1` (controlled Helm cutover automation)
## Stage 11
- no new business REST routes
- asterisk-bridge-service call-control routes:
- `/asterisk/live-calls`
- `/asterisk/live-calls/{call_id}/claim`
- `/asterisk/live-calls/{call_id}/hangup`
- `/asterisk/live-calls/{call_id}/blind-transfer`
- `/asterisk/live-calls/{call_id}/actions`
- operator shell uses these routes for live call handling with external softphone media
## Stage 12
- no new business REST routes
- additive operator UX route:
- `/asterisk/recent-calls`
- operator shell uses this route for the short-lived recent-calls bucket
## Stage 14
- no new business REST routes
- additive browser softphone config route:
- `/asterisk/browser-softphone/config`
- operator shell may use direct browser media over Asterisk WSS in QA
## Stage 15
- telegram-adapter-service real-bot webhook:
- `/integrations/telegram/bot/webhook`
- telegram-adapter-service operator thread workspace:
- `/integrations/telegram/threads`
- `/integrations/telegram/threads/{thread_id}`
- `/integrations/telegram/threads/{thread_id}/messages`
- `/integrations/telegram/threads/{thread_id}/claim`
- `/integrations/telegram/threads/{thread_id}/close`
- `/integrations/telegram/threads/{thread_id}/escalate`
- operator shell gets a separate `Telegram` page as the canonical surface for this channel
+59
View File
@@ -0,0 +1,59 @@
# Asterisk UserEvent Contract
Track 9 uses dialplan-generated `UserEvent` frames over AMI as the stable bridge contract
between Asterisk and the platform.
The bridge listens only for `UserEvent` names starting with `MVPCC`.
## `MVPCCCallStarted`
Required fields:
- `CallID`
- `LinkedID`
- `CallerNumber`
- `CallerName`
- `QueueCode`
- `Extension`
- `Context`
- `Direction`
Example:
```text
Event: UserEvent
UserEvent: MVPCCCallStarted
CallID: 1740912000.12
LinkedID: 1740912000.12
CallerNumber: 1001
CallerName: Lab Caller
QueueCode: voice_lab
Extension: 7000
Context: from-softphones
Direction: inbound
```
## `MVPCCCallEnded`
Required fields:
- `CallID`
- `LinkedID`
- `HangupCause`
- `DurationSeconds`
## `MVPCCRecordingReady`
Required fields:
- `CallID`
- `LinkedID`
- `RemotePath`
- `FileName`
- `MimeType`
- `DurationSeconds`
Notes:
- `RemotePath` is the absolute path on the Asterisk Linux VM.
- The platform downloads the file over SFTP, then uploads it into `recording-service`.
@@ -0,0 +1,44 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "call.recording.ready.json",
"title": "call.recording.ready",
"type": "object",
"required": ["event_type", "call_id", "payload"],
"properties": {
"event_type": {
"const": "recording.ready"
},
"call_id": {
"type": "string",
"minLength": 1
},
"interaction_id": {
"type": ["string", "null"]
},
"payload": {
"type": "object",
"required": ["source_path"],
"properties": {
"source_path": {
"type": "string",
"minLength": 1
},
"file_name": {
"type": ["string", "null"]
},
"mime_type": {
"type": ["string", "null"]
},
"duration_seconds": {
"type": ["integer", "null"],
"minimum": 0
},
"recorded_at": {
"type": ["string", "null"]
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mvp-cc.local/docs/architecture/event-envelope.json",
"title": "EventEnvelope",
"type": "object",
"required": [
"event_id",
"event_type",
"event_version",
"occurred_at",
"producer",
"entity_type",
"entity_id",
"routing_key",
"payload"
],
"properties": {
"event_id": {"type": "string", "minLength": 1},
"event_type": {"type": "string", "minLength": 1},
"event_version": {"type": "integer", "minimum": 1},
"occurred_at": {"type": "string", "format": "date-time"},
"producer": {"type": "string", "minLength": 1},
"entity_type": {"type": "string", "minLength": 1},
"entity_id": {"type": "string", "minLength": 1},
"correlation_id": {"type": ["string", "null"]},
"routing_key": {"type": "string", "minLength": 1},
"payload": {"type": "object"}
},
"additionalProperties": true
}
+37
View File
@@ -0,0 +1,37 @@
# Event Schema Index
- `interaction.created.json`
- `interaction.assigned.json`
- `interaction.escalated.json`
- `interaction.closed.json`
- `agent.state.changed.json`
- `call.recording.ready.json`
- `call.connected.json`
- `call.transferred.json`
- `ivr.completed.json`
Each file is a JSON Schema (draft 2020-12) and can be used as a validation contract between services.
Track 8 introduces a canonical transport envelope for bus-delivered events:
- `docs/architecture/event-envelope.json`
Payload-specific schemas under `contracts/events` remain the authoritative contracts for the
domain payload inside that envelope.
Track 4 now provides the concrete schema file for the recording import placeholder:
- `docs/architecture/call.recording.ready.json`
Track 5 adds the concrete schema for IVR completion:
- `docs/architecture/ivr.completed.json`
Track 9 adds the dialplan-to-platform `UserEvent` contract reference:
- `docs/architecture/asterisk-user-events.md`
Track 11 adds additive voice lifecycle event types produced by call-control flow:
- `call.connected` (operator connected to customer)
- `call.transferred` (blind transfer requested by operator/admin)
+54
View File
@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mvp-cc.local/contracts/events/ivr.completed.json",
"title": "ivr.completed",
"type": "object",
"required": ["event_type", "call_id", "payload"],
"properties": {
"event_type": {
"const": "ivr.completed"
},
"call_id": {
"type": "string",
"minLength": 1
},
"interaction_id": {
"type": ["string", "null"]
},
"payload": {
"type": "object",
"required": ["session_id", "flow_id", "outcome_code", "resolved_queue_id", "digits", "terminal_node_id"],
"properties": {
"session_id": {
"type": "string",
"minLength": 1
},
"flow_id": {
"type": "string",
"minLength": 1
},
"outcome_code": {
"type": "string",
"minLength": 1
},
"resolved_queue_id": {
"type": "string",
"minLength": 1
},
"digits": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[0-9]$"
}
},
"terminal_node_id": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
+25
View File
@@ -0,0 +1,25 @@
# Architecture Overview
## Services
- api-gateway
- auth-service
- audit-service
- customer-service
- interaction-service
- routing-service
- voice-adapter-service
- telegram-adapter-service
- kb-service
- reporting-service
- supervisor-service
## Data and event strategy
- All services use SQL storage via `DATABASE_URL`
- Default local mode: SQLite (`.data/mvp_cc.db`)
- Target mode: PostgreSQL (supported in `deployment/docker-compose.yml`)
- Contract-first API and JSON event schema artifacts
- Event contracts are fixed and ready for message bus integration (RabbitMQ/Kafka) in next phase
## Deployment
- Local: Docker Compose
- On-prem: Kubernetes manifests and Helm chart
+744
View File
@@ -0,0 +1,744 @@
# Voice AI V1 Technical Design
Status: `Proposed`
## 1. Goal
Add an AI-first voice operator on top of the accepted Asterisk voice baseline without replacing the current telephony stack.
Voice AI V1 must:
- answer selected inbound calls before a human joins;
- reuse the existing `interaction`, `customer`, `KB`, `queue`, and operator UI model;
- keep Asterisk as the system of record for call lifecycle, transfer, recording, SIP/WebRTC, and queue ownership;
- store business state in platform tables, not only inside model context;
- support deterministic AI-to-human handoff into the existing human queue.
## 2. Non-goals
Voice AI V1 does not:
- replace Asterisk, AMI, dialplan, queueing, SIP/WebRTC, or recording;
- introduce a separate AI telephony platform or second source of truth for calls;
- push full CRM history, full call transcript, or raw recordings into every model turn;
- introduce a multi-agent swarm;
- add attended transfer, conference, or full human-to-AI return in the same live call.
`return-to-ai` after human takeover is intentionally deferred to V2. It requires a second telephony redirect and fresh media re-attachment, which is riskier than AI-first plus human handoff.
## 3. Existing Baseline We Build On
The current repo already has the pieces needed for Voice AI V1:
- `asterisk_bridge_service` owns AMI ingestion, live call tracking, claim/hangup/transfer, and recording import.
- `voice_adapter_service` stores telephony lifecycle events.
- `interaction_service` owns the canonical interaction lifecycle.
- `customer_service` plus `customer_external_identities` already provide cross-channel identity linking.
- `ai_orchestrator_service` already implements the Telegram AI pattern:
- deterministic orchestration;
- explicit AI disclosure;
- `ai_sessions` and `ai_turns`;
- AI-to-human handoff;
- source-of-truth separation between channel service and AI service.
- `/operator` already has a hybrid Telegram AI UX with AI badges and AI summary cards.
Voice AI V1 should copy the Telegram AI service boundary pattern, not the Telegram channel model itself.
## 4. Target Architecture
### 4.1 Service responsibilities
| Service | Role in Voice AI V1 |
| --- | --- |
| `asterisk_bridge_service` | Keeps telephony truth, selects AI path for eligible calls, starts/stops AI runtime sessions, performs AI-to-human redirect, exposes operator-facing voice AI summary. |
| `ai_voice_runtime_service` | New realtime service. Owns media session state, ASR/TTS streaming, turn detection, barge-in, runtime latency budget, and handoff requests. |
| `ai_orchestrator_service` | Reused and extended. Owns deterministic voice decisioning, filtered context assembly, KB/tool usage, safe business policies, and AI summary generation. |
| `interaction_service` | Remains the owner of interaction status and timeline; gains an internal timeline append endpoint for additive AI events. |
| `voice_adapter_service` | Stays the owner of telephony event persistence only. It does not become the AI runtime. |
| `ui/operator` | Reuses the current live call and popup flow; adds voice AI badges and handoff summary, but no separate `AI telephony` workspace. |
### 4.2 High-level component view
```mermaid
flowchart LR
A["Asterisk<br/>queue, redirect, recording, SIP/WebRTC"] --> B["asterisk_bridge_service<br/>call truth + control plane"]
B --> C["ai_voice_runtime_service<br/>media bridge + ASR/TTS + barge-in"]
C --> D["ai_orchestrator_service<br/>policy + tools + summaries"]
D --> E["customer_service / shared DB<br/>customer profile + memory"]
D --> F["interaction_service<br/>interaction status + timeline"]
D --> G["kb_service<br/>KB lookup"]
B --> H["operator UI<br/>live call popup + voice AI summary"]
```
### 4.3 Source-of-truth rule
- Telephony truth stays in `AsteriskCallLinkRow`, AMI events, queue redirect, and recording flow.
- AI truth stays in `voice_ai_sessions`, `ai_sessions`, `ai_turns`, and transcript rows.
- Interaction truth stays in `Interaction` plus `InteractionTimeline`.
- Customer truth stays in `Customer` plus `CustomerExternalIdentity`.
No single prompt becomes the long-term state container.
## 5. Call Flow
### 5.1 AI-first inbound voice flow
```mermaid
sequenceDiagram
participant A as Asterisk
participant B as asterisk_bridge_service
participant R as ai_voice_runtime_service
participant O as ai_orchestrator_service
participant I as interaction_service
A->>B: MVPCCCallStarted
B->>I: create / reuse interaction
B->>B: create or update AsteriskCallLinkRow
B->>R: POST /internal/voice-ai/sessions
R->>O: POST /ai/voice/sessions/{id}/start
R->>A: play AI disclosure + greeting
A->>R: caller audio stream
R->>O: POST /ai/voice/sessions/{id}/turns
O-->>R: reply plan or handoff decision
R->>A: TTS playback
```
Detailed steps:
1. `asterisk_bridge_service` receives the existing `MVPCCCallStarted` event.
2. It keeps the current behavior of creating or resolving `interaction_id` and `AsteriskCallLinkRow`.
3. It evaluates whether this queue or IVR outcome should go to AI-first mode.
4. If not AI-eligible, the current human flow stays unchanged.
5. If AI-eligible:
- create `voice_ai_session`;
- create or link generic `ai_session` with `channel="voice"`;
- mark the call row as AI-owned;
- start a runtime control session in `ai_voice_runtime_service`;
- attach the customer leg to the AI media bridge.
6. `ai_voice_runtime_service` plays a short disclosure and greeting.
7. Finalized caller utterances are sent to `ai_orchestrator_service`.
8. The orchestrator loads filtered business context, runs deterministic policy and KB/tool lookup, and returns a structured decision.
9. The runtime either:
- plays TTS back to the caller; or
- requests handoff to a human queue.
### 5.2 AI-to-human handoff
```mermaid
sequenceDiagram
participant R as ai_voice_runtime_service
participant O as ai_orchestrator_service
participant B as asterisk_bridge_service
participant A as Asterisk
participant U as Operator UI
R->>O: POST voice turn
O-->>R: needs_handoff=true + reason + summary
R->>B: POST /internal/voice-ai/calls/{call_id}/handoff
B->>B: mark ai_state=handoff_required
B->>A: redirect caller leg to existing human queue
A->>U: normal operator incoming call path
U->>B: GET /asterisk/live-calls/{call_id}/ai-summary
B-->>U: AI handoff summary
```
Handoff rules:
- handoff happens only through the existing Asterisk queue path;
- the AI runtime never calls an operator directly and never becomes a second queueing owner;
- `interaction_id` stays the same through AI and human phases;
- the operator receives a concise AI summary before or during claim.
### 5.3 Call end
- `MVPCCCallEnded` and `MVPCCRecordingReady` continue to come from Asterisk through the existing bridge.
- The bridge informs the runtime session that telephony ended.
- The runtime closes `voice_ai_session`.
- The orchestrator writes the final AI summary and marks `ai_session` closed.
- The interaction timeline gets final AI events if they were not written yet.
## 6. Queue Selection and AI Eligibility
Voice AI V1 should start with config-driven AI eligibility, not a new admin UI.
Recommended V1 config:
`AI_VOICE_QUEUE_CONFIG_JSON`
Example shape:
```json
{
"voice_lab": {
"mode": "ai_first",
"agent_profile": "voice_support",
"handoff_queue_code": "voice_lab",
"language": "ru"
}
}
```
Why config-first:
- it avoids touching the accepted routing UI and queue schema in the first step;
- it keeps rollout reversible per queue;
- it matches the current repo style where telephony behavior is still largely env-driven.
The longer-term path can move this policy into routing/admin later.
## 7. Data Model
## 7.1 Reused tables
These tables stay authoritative and should be reused:
- `interactions`
- `interaction_timelines`
- `customer_external_identities`
- `asterisk_call_links`
- `voice_events`
- `call_recordings`
- `ai_sessions`
- `ai_turns`
Voice customer linking should reuse `customer_external_identities` with:
- `channel = "voice"`
- `external_subject = normalized caller number`
No new identity table is needed.
## 7.2 New table: `voice_ai_sessions`
This table stores runtime state that does not fit cleanly into generic `ai_sessions`.
Suggested columns:
| Column | Type | Purpose |
| --- | --- | --- |
| `id` | integer pk | Internal row id |
| `session_id` | string unique | Public runtime session id |
| `call_id` | string unique index | Current Asterisk customer-leg call id |
| `linked_id` | string index | Asterisk linked id for recovery if call id changes |
| `interaction_id` | string index | Shared interaction |
| `customer_id` | string index nullable | Linked customer |
| `queue_id` | string index | Original platform queue |
| `ai_session_id` | string index | Generic AI session link |
| `agent_profile` | string index | Voice policy profile |
| `status` | string index | `queued`, `greeting`, `listening`, `thinking`, `speaking`, `handoff_requested`, `human_owned`, `completed`, `error` |
| `language` | string index nullable | Current active language |
| `asr_provider` | string nullable | Selected ASR backend |
| `tts_provider` | string nullable | Selected TTS backend |
| `handoff_reason` | text nullable | Last handoff reason |
| `handoff_target_queue_id` | string nullable | Human target queue |
| `disclosure_played_at` | string nullable | When AI disclosure was first spoken |
| `last_user_utterance_at` | string nullable | Latest finalized caller speech |
| `last_ai_reply_at` | string nullable | Latest completed AI reply |
| `started_at` | string index | Session start |
| `updated_at` | string index | Last state update |
| `ended_at` | string index nullable | Session end |
## 7.3 New table: `voice_transcript_segments`
This table stores finalized voice transcript units separately from generic AI turns.
Suggested columns:
| Column | Type | Purpose |
| --- | --- | --- |
| `id` | integer pk | Internal row id |
| `segment_id` | string unique | Public segment id |
| `session_id` | string index | `voice_ai_sessions.session_id` |
| `call_id` | string index | Voice call correlation |
| `interaction_id` | string index | Interaction correlation |
| `speaker` | string index | `caller`, `assistant`, `system`, `operator` |
| `source_type` | string index | `asr`, `tts`, `handoff_summary`, `system` |
| `sequence_no` | integer index | Ordered segment number |
| `text` | text | Final transcript text |
| `confidence` | number nullable | ASR confidence when applicable |
| `is_final` | boolean | Finalized transcript only in V1, but keep the flag for future partials |
| `barge_in_interrupted` | boolean | Whether the assistant segment was interrupted |
| `payload_json` | text | Provider metadata, timestamps, tool refs |
| `created_at` | string index | Write time |
V1 should store finalized transcript segments only. Partial ASR events can stay in memory inside the runtime.
## 7.4 Additive columns on `asterisk_call_links`
This mirrors the Telegram pattern where the channel source-of-truth row also exposes AI status.
Add:
- `voice_session_id VARCHAR(64) NULL`
- `ai_session_id VARCHAR(64) NULL`
- `ai_state VARCHAR(32) NULL`
- `ai_handoff_reason TEXT NULL`
- `ai_last_model_at VARCHAR(64) NULL`
Suggested `ai_state` values:
- `queued`
- `greeting`
- `listening`
- `thinking`
- `active`
- `handoff_required`
- `human_owned`
- `closed`
- `error`
This lets `/asterisk/live-calls` and `/asterisk/recent-calls` drive UI chips directly without extra joins on every poll.
## 7.5 Additive column on `ai_sessions`
Add:
- `call_id VARCHAR(128) NULL`
Reason:
- Telegram already uses `thread_id`;
- voice needs a direct telephony key for fast lookup and summary generation;
- this keeps `ai_sessions` truly cross-channel instead of Telegram-shaped.
## 7.6 Reuse of `ai_turns`
Reuse `ai_turns` for:
- finalized user turn seen by the orchestrator;
- model reply plan;
- tool invocation results;
- summary turn written during handoff or closure.
Do not reuse `ai_jobs` for voice V1. Telegram jobs are thread-triggered and synchronous voice turn processing does not need that queue model.
## 8. API Design
## 8.1 `asterisk_bridge_service` -> `ai_voice_runtime_service`
New internal control-plane endpoints:
### `POST /internal/voice-ai/sessions`
Starts a runtime session for an already accepted telephony call.
Request:
```json
{
"call_id": "1740912000.12",
"linked_id": "1740912000.12",
"interaction_id": "int_...",
"queue_id": "que_...",
"caller_number": "+7701...",
"caller_name": "Lab Caller",
"agent_profile": "voice_support",
"language_hint": "ru",
"handoff_queue_id": "que_...",
"metadata": {
"queue_code": "voice_lab",
"direction": "inbound"
}
}
```
Response:
```json
{
"voice_session_id": "avs_...",
"ai_session_id": "ais_...",
"status": "queued"
}
```
### `POST /internal/voice-ai/sessions/{session_id}/telephony-events`
Bridge notifies runtime about:
- `call.connected`
- `call.ended`
- `recording.ready`
- `operator.connected`
This keeps telephony truth in the bridge while runtime stays current.
## 8.2 `ai_voice_runtime_service` -> `ai_orchestrator_service`
### `POST /ai/voice/sessions/{session_id}/start`
Creates or reopens the generic AI session and returns greeting policy:
```json
{
"voice_session_id": "avs_...",
"call_id": "1740912000.12",
"interaction_id": "int_...",
"customer_id": "cus_...",
"language_hint": "ru",
"agent_profile": "voice_support"
}
```
Response:
```json
{
"session_id": "ais_...",
"language": "ru",
"greeting_text": "Здравствуйте. Я AI-оператор компании...",
"disclosure_required": true
}
```
### `POST /ai/voice/sessions/{session_id}/turns`
Main deterministic decision endpoint.
Request:
```json
{
"voice_session_id": "avs_...",
"call_id": "1740912000.12",
"interaction_id": "int_...",
"transcript_text": "Хочу узнать статус заявки",
"language": "ru",
"sequence_no": 3,
"barge_in": false,
"metadata": {
"turn_duration_ms": 4200
}
}
```
Response:
```json
{
"language": "ru",
"intent": "status_check",
"reply_text": "Я AI-оператор компании. Проверяю данные по обращению...",
"confidence": 0.83,
"needs_handoff": false,
"handoff_reason": null,
"case_action": "keep_open",
"kb_refs": ["art_..."],
"summary_text": "Клиент уточняет статус заявки.",
"model": "gpt-4o-mini",
"latency_ms": 780
}
```
`case_action` should stay aligned with the existing Telegram pattern:
- `none`
- `keep_open`
- `close`
- `escalate`
### `POST /ai/voice/sessions/{session_id}/close`
Finalizes AI state and writes the terminal summary.
## 8.3 `ai_voice_runtime_service` -> `asterisk_bridge_service`
### `POST /internal/voice-ai/calls/{call_id}/handoff`
Requests redirect of the current customer leg into the existing human queue.
Request:
```json
{
"voice_session_id": "avs_...",
"ai_session_id": "ais_...",
"interaction_id": "int_...",
"target_queue_id": "que_...",
"reason": "Нужен человек для чувствительного запроса.",
"summary": {
"customer_request_text": "Клиент просит изменить договор",
"ai_outcome_text": "AI собрал контекст и не выполнял чувствительное действие",
"recommended_next_step": "Проверить договор и продолжить вручную"
}
}
```
Behavior:
- bridge validates the call is still active;
- bridge updates `ai_state` to `handoff_required`;
- bridge appends timeline `ai.handoff_requested`;
- bridge redirects the customer leg into the configured human queue;
- when the normal operator-connected flow happens, the same call row becomes `human_owned`.
This endpoint is internal-only and trusted for service actors such as `svc:ai-voice-runtime`.
## 8.4 `interaction_service`
Add one internal endpoint:
### `POST /interactions/{interaction_id}/timeline`
Request:
```json
{
"action": "ai.reply_generated",
"metadata": {
"call_id": "1740912000.12",
"voice_session_id": "avs_...",
"ai_session_id": "ais_..."
}
}
```
Why add this now:
- Voice AI should not write cross-service timeline rows by reaching into another service's DB contract ad hoc;
- the same endpoint can later be reused by Telegram AI without changing its current behavior immediately;
- it makes the AI event contract explicit.
Required Voice AI timeline actions:
- `ai.session_started`
- `ai.reply_generated`
- `ai.handoff_requested`
- `ai.handoff_completed`
- `ai.error`
Existing interaction endpoints remain reused as-is:
- `PATCH /interactions/{id}/status`
- `POST /interactions/{id}/escalate`
- `PATCH /interactions/{id}/assign`
## 8.5 Operator-facing read APIs
Extend `asterisk_bridge_service` output:
### `GET /asterisk/live-calls`
Add to each row:
- `voice_session_id`
- `ai_session_id`
- `ai_state`
- `ai_handoff_reason`
- `ai_last_model_at`
### `GET /asterisk/recent-calls`
Expose the same additive AI fields.
### `GET /asterisk/live-calls/{call_id}/ai-summary`
Return a summary shape intentionally aligned with Telegram:
```json
{
"call_id": "1740912000.12",
"session_id": "ais_...",
"voice_session_id": "avs_...",
"status_label": "AI передал звонок оператору",
"status_tone": "handoff",
"customer_request_text": "Клиент хочет узнать статус обращения и изменить способ оплаты",
"ai_outcome_text": "AI собрал контекст и объяснил рамки, но не выполнил чувствительное действие",
"handoff_reason": "Запрос требует человека и проверки вручную",
"recommended_next_step": "Проверить карточку обращения и продолжить звонок вручную",
"generated_at": "2026-03-09T12:34:56Z"
}
```
This should be produced from `voice_ai_sessions`, `ai_turns`, and the latest transcript segments.
## 9. Context Filtering Rules
Voice AI must not send the whole call history into the model each turn.
For every voice turn, `ai_orchestrator_service` should assemble a filtered context from:
- customer profile:
- `customer_id`
- display name
- preferred phone
- tags
- customer memory:
- recent resolved issues
- notable preferences
- interaction state:
- `interaction_id`
- status
- queue
- last relevant timeline events
- voice session state:
- language
- disclosure already played or not
- previous handoff flag
- current turn number
- recent transcript window:
- last `6-10` finalized segments, not the entire transcript
- KB:
- top `3` matched articles or fewer
- business policy:
- allowed actions
- mandatory disclosure
- sensitive-topic escalation rules
This matches the Telegram AI design principle already present in the repo.
## 10. Runtime Behavior
## 10.1 ASR/TTS abstraction
`ai_voice_runtime_service` should expose provider interfaces and start with one configured provider per environment.
Recommended internal modules:
- `providers/asr.py`
- `providers/tts.py`
- `session_manager.py`
- `media_bridge.py`
- `barge_in.py`
V1 supports one active ASR provider and one active TTS provider, behind interfaces. Multi-provider fallback is not required in the first version.
## 10.2 Barge-in
Barge-in is a V1 requirement because voice UX breaks if the caller cannot interrupt TTS.
Required behavior:
- while TTS is playing, incoming speech activity stops or fades out current playback;
- interrupted assistant output is marked with `barge_in_interrupted=true` in transcript;
- only finalized caller speech creates a new orchestrator turn;
- if interruption happens repeatedly or ASR confidence is poor, handoff rules may trigger.
## 10.3 Latency budget
Target budget for one AI turn:
- end-of-utterance to finalized ASR text: `<= 600 ms`
- orchestrator decision: `<= 900 ms`
- first TTS audio chunk: `<= 500 ms`
- total pause before AI speech starts: `<= 2.0 s`
If the runtime cannot stay within the budget repeatedly, it should prefer human handoff over a degraded long-silence experience.
## 11. Operator UI Integration
Voice AI V1 should integrate into the current operator shell, not create a second voice console.
### 11.1 Existing surfaces to extend
- browser call popup overlay;
- `Voice debug` live/recent calls list;
- unified customer history on `/operator`.
### 11.2 Required UI changes
1. Extend live call rows with an AI chip using the same language as Telegram:
- `AI active`
- `Ждёт человека`
- `AI error`
2. When a transferred AI-owned call reaches the operator popup:
- load `GET /asterisk/live-calls/{call_id}/ai-summary`;
- show a compact `Сводка AI` card above call actions;
- keep existing `Принять в работу`, `Передать`, `Завершить` controls unchanged.
3. Add AI metadata to customer history:
- AI session started
- AI handoff requested
- AI handoff completed
4. Do not add a separate full transcript workspace in V1.
### 11.3 UI behavior intentionally deferred
Deferred to V2:
- operator button `Вернуть AI` for live voice calls;
- inline live transcript for operators during the call;
- supervisor transcript explorer for full recordings plus transcripts.
## 12. Deployment and Config
## 12.1 New service in `docker-compose.server.yml`
Add:
- `ai-voice-runtime-service`
New shared env:
- `AI_VOICE_RUNTIME_SERVICE_URL`
- `AI_VOICE_ENABLED`
- `AI_VOICE_QUEUE_CONFIG_JSON`
- `AI_VOICE_ASR_PROVIDER`
- `AI_VOICE_TTS_PROVIDER`
- `AI_VOICE_TTS_CACHE_ENABLED`
- `AI_VOICE_TTS_CACHE_DIR`
- `AI_VOICE_MAX_CONTEXT_SEGMENTS`
- `AI_VOICE_HANDOFF_TIMEOUT_SECONDS`
Service auth:
- add trusted subject `svc:ai-voice-runtime` where internal bridge endpoints require it.
## 12.2 Asterisk-side change
The accepted human voice path stays intact.
Voice AI adds one new AI media bridge path in dialplan only for AI-selected queues. Preferred V1 implementation is:
- Asterisk dialplan redirects the customer leg into an external media bridge context dedicated to AI.
Exact low-level primitive should be validated in the lab:
- preferred: `AudioSocket` or equivalent bidirectional audio bridge;
- fallback: narrowly scoped external-media/ARI only for AI queues.
The choice must keep Asterisk as the telephony owner.
## 13. Rollout Order
Recommended implementation order:
1. schema changes only:
- `voice_ai_sessions`
- `voice_transcript_segments`
- additive AI columns on `asterisk_call_links`
- additive `call_id` on `ai_sessions`
2. control plane only:
- bridge starts and closes empty runtime sessions behind `AI_VOICE_ENABLED=0`
3. lab media path:
- one AI-enabled lab queue
- disclosure + greeting + ASR/TTS echo flow
4. orchestrator integration:
- KB lookup
- filtered context
- handoff decisioning
5. operator summary:
- popup card
- voice debug AI chips
6. staged pilot rollout per queue.
## 14. Key Risks and Open Questions
1. `call_id` stability during AI-to-human redirect must be validated in the Asterisk lab.
If redirect creates a new call id, bridge recovery must switch to `linked_id` first and only then reuse `interaction_id`.
2. The exact Asterisk media primitive must be confirmed before implementation.
The design assumes a bidirectional bridge is available without replacing the accepted telephony baseline.
3. Provider latency must be measured in the target environment before enabling AI-first for production queues.
4. Sensitive actions should stay read-only in V1.
Voice AI should use KB, customer lookup, and safe interaction updates, but not execute risky external business actions directly.
## 15. Summary
Voice AI V1 should be implemented as an additive AI layer over the accepted voice baseline:
- `asterisk_bridge_service` remains telephony truth and handoff executor;
- `ai_voice_runtime_service` is the new realtime media layer;
- `ai_orchestrator_service` is reused for deterministic business decisioning;
- `interaction_service` remains the owner of the canonical AI timeline;
- operator UI gets AI badges and AI handoff summary, not a new telephony product.
This keeps the current live voice contour intact while adding the same AI-first and human-handoff architecture that already works in Telegram.