From 6035c5d4d014c6d1d78937b4459d0a1049699950 Mon Sep 17 00:00:00 2001 From: Yera All Date: Tue, 7 Apr 2026 11:01:17 +0500 Subject: [PATCH] feat(ai): shift name extraction to LLM payload piggybacking to improve contextual accuracy --- README.md | 373 ------------------ services/ai_orchestrator_service/app.py | 1 + .../operator_persona.py | 2 +- services/ai_orchestrator_service/voice.py | 46 ++- test_get_summary.py | 28 ++ 5 files changed, 71 insertions(+), 379 deletions(-) delete mode 100644 README.md create mode 100644 test_get_summary.py diff --git a/README.md b/README.md deleted file mode 100644 index 9571539..0000000 --- a/README.md +++ /dev/null @@ -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\ -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 -python scripts\track9_check.py --base-url http://127.0.0.1:8080 --database-url --require-recording -python scripts\track9_collect_evidence.py --base-url http://127.0.0.1:8080 --database-url --run-checks --require-recording -powershell -ExecutionPolicy Bypass -File scripts\track9_2_cutover.ps1 -KubeContext -Namespace -Release -GatewayBaseUrl http:// -DatabaseUrl postgresql://<...> -ImageTag -AmiHost -AmiUser -AmiSecret -SftpHost -SftpUser -SftpPassword -QueueId -AppTokenSecret -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://:8080 -python scripts\finalize_mvp_pilot.py --session-dir docs/uat/evidence/manual_ --dry-run -python scripts\finalize_mvp_pilot.py --session-dir docs/uat/evidence/manual_ -``` - -## 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`. diff --git a/services/ai_orchestrator_service/app.py b/services/ai_orchestrator_service/app.py index 848ab51..fbbb239 100644 --- a/services/ai_orchestrator_service/app.py +++ b/services/ai_orchestrator_service/app.py @@ -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, diff --git a/services/ai_orchestrator_service/operator_persona.py b/services/ai_orchestrator_service/operator_persona.py index 862d6a8..720d8ef 100644 --- a/services/ai_orchestrator_service/operator_persona.py +++ b/services/ai_orchestrator_service/operator_persona.py @@ -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." ) diff --git a/services/ai_orchestrator_service/voice.py b/services/ai_orchestrator_service/voice.py index 806233d..58b4f50 100644 --- a/services/ai_orchestrator_service/voice.py +++ b/services/ai_orchestrator_service/voice.py @@ -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, diff --git a/test_get_summary.py b/test_get_summary.py new file mode 100644 index 0000000..3e2746c --- /dev/null +++ b/test_get_summary.py @@ -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()