feat(ai): shift name extraction to LLM payload piggybacking to improve contextual accuracy

This commit is contained in:
Yera All
2026-04-07 11:01:17 +05:00
parent 1444cf07ab
commit 6035c5d4d0
5 changed files with 71 additions and 379 deletions
-373
View File
@@ -1,373 +0,0 @@
# 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`.
+1
View File
@@ -2402,6 +2402,7 @@ def _sanitize_decision(raw: dict[str, Any], *, fallback_language: str) -> dict[s
"language": str(raw.get("language") or fallback_language or "ru"),
"intent": str(raw.get("intent") or "unknown"),
"reply_text": str(raw.get("reply_text") or "").strip(),
"extracted_name": str(raw.get("extracted_name") or "").strip() or None,
"confidence": float(raw.get("confidence") or 0.0),
"needs_handoff": bool(raw.get("needs_handoff")),
"handoff_reason": str(raw.get("handoff_reason") or "").strip() or None,
@@ -75,6 +75,6 @@ def operator_system_prompt(*, language: str, channel_label: str, is_voice: bool)
"If the customer explicitly asks for a live operator, if the request is sensitive, or if the case is blocked, "
"set needs_handoff=true. "
f"{delivery_hint} "
"Return only a JSON object with keys: language, intent, reply_text, confidence, needs_handoff, "
"Return only a JSON object with keys: language, intent, reply_text, extracted_name, confidence, needs_handoff, "
"handoff_reason, case_action, kb_refs. case_action must be one of none, close, escalate, keep_open."
)
+41 -5
View File
@@ -1115,14 +1115,23 @@ def _voice_llm_prompt_messages(
"kb_results": kb_context,
"history": history,
}
system_prompt = persona.operator_system_prompt(
language=language,
channel_label="Voice",
is_voice=True,
)
if name_status in ("name_not_obtained", "name_followup_required"):
system_prompt += (
" The user's name is not yet obtained. If the user explicitly provided their name in this turn, "
"extract it into the `extracted_name` JSON field. If they did not provide a name or just stated a problem "
"(e.g., 'У меня не работает интернет'), set `extracted_name` to null and politely ask for their name "
"in your `reply_text` before assisting."
)
return [
{
"role": "system",
"content": persona.operator_system_prompt(
language=language,
channel_label="Voice",
is_voice=True,
),
"content": system_prompt,
},
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
]
@@ -1951,6 +1960,33 @@ def turn_voice_session(session_id: str, payload: VoiceAITurnIn) -> VoiceAITurnDe
customer_name_value=name_update["value"],
customer_name_status=name_update["status"],
)
if decision.get("extracted_name") and name_update["status"] not in ("name_obtained", "name_obtained_previously"):
name_update["status"] = "name_obtained"
name_update["value"] = str(decision["extracted_name"]).strip()
name_update["source"] = "llm_extraction"
name_update["resolved_at"] = now
name_update["inline_followup"] = False
finalized_name = _finalize_customer_name(
session,
customer=customer,
customer_id=customer_id,
call_id=payload.call_id,
final_name=name_update["value"],
resolved_at=now,
)
if finalized_name:
name_update["value"] = finalized_name
_persist_voice_name_state(
session,
voice_session=voice_session,
status=name_update["status"],
value=name_update["value"],
source=name_update["source"],
resolved_at=name_update["resolved_at"],
)
decision_metadata = _voice_name_metadata(
language=voice_session.voice_start_language or ai_session.language or "ru",
customer_id=customer_id,
+28
View File
@@ -0,0 +1,28 @@
import urllib.request
import json
import traceback
try:
req = urllib.request.Request(
'http://92.38.48.166:8080/proxy/auth/auth/login',
data=json.dumps({'username': 'admin', 'password': 'admin123'}).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
token = json.loads(urllib.request.urlopen(req).read().decode()).get('access_token')
headers = {'Authorization': f'Bearer {token}'}
req = urllib.request.Request('http://92.38.48.166:8080/proxy/asterisk-bridge/asterisk/events?limit=250', headers=headers)
events = json.loads(urllib.request.urlopen(req).read().decode())
cids = []
for e in events:
if e['call_id'] not in cids: cids.append(e['call_id'])
for cid in cids[:1]:
summ_req = urllib.request.Request(f'http://92.38.48.166:8080/proxy/asterisk-bridge/asterisk/live-calls/{cid}/ai-summary', headers=headers)
summ = json.loads(urllib.request.urlopen(summ_req).read().decode())
print('Call ID:', cid)
print('Voice Session ID:', summ.get('voice_session_id'))
for t in summ.get('interaction_transcript', []):
print(f"[{t.get('sequence_no', '')}] {t['speaker']}: {t['text']}")
except Exception as e:
traceback.print_exc()