Files
call-center/scripts/demo_seed.py

565 lines
22 KiB
Python

from __future__ import annotations
import argparse
from datetime import datetime, timezone
import json
import math
import os
from pathlib import Path
import struct
from typing import Any
import wave
import httpx
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = ROOT / ".local_stack" / "demo-seed-summary.json"
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def now_stamp() -> str:
return utc_now().strftime("%Y%m%d_%H%M%S")
def build_seed_plan(tag: str) -> dict[str, Any]:
return {
"queue_name": "Demo Queue",
"customer_name": "Demo Customer",
"customer_phone": "+77010000001",
"voice_subject": "Demo voice escalation",
"chat_subject": "Demo Telegram follow-up",
"webchat_text": "Demo webchat request from the website",
"email_subject": "Demo email request about account access",
"email_body": "Please restore access to the portal. This message is preloaded for the management demo.",
"kb_category": "Demo Knowledge",
"kb_title": "Demo Knowledge Article",
"kb_keyword": "demo-showcase",
"telegram_text": "Demo inbound Telegram message",
"voice_event_type": "call.started",
"recording_file_name": "demo-call.wav",
"ivr_root_prompt_kz": "\u0421\u0430\u043b\u0430\u043c\u0430\u0442\u0441\u044b\u0437 \u0431\u0430! \u049a\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d \u0442\u0430\u04a3\u0434\u0430\u0443 \u04af\u0448\u0456\u043d \u0431\u0456\u0440 \u0446\u0438\u0444\u0440\u044b\u043d \u0442\u0435\u0440\u0456\u04a3\u0456\u0437.",
"ivr_root_prompt_ru": "\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0446\u0435\u043d\u0442\u0440. \u0414\u043b\u044f \u0440\u0443\u0441\u0441\u043a\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0446\u0438\u0444\u0440\u0443 \u0434\u0432\u0430.",
"ivr_root_prompt": "Здравствуйте! Добро пожаловать в контакт-центр. Для русского языка нажмите цифру два. Саламатсыз ба! Қазақ тілін таңдау үшін бір цифрын теріңіз.",
"ivr_ru_menu_prompt": "Для отдела продаж нажмите 1. Для службы поддержки нажмите 2.",
"ivr_kz_menu_prompt": "Сату бөлімі үшін 1 басыңыз. Қолдау қызметі үшін 2 басыңыз.",
}
def _demo_ivr_flow_document(
plan: dict[str, Any],
*,
voice_start_kz_queue_id: str,
voice_start_ru_queue_id: str,
) -> dict[str, Any]:
return {
"nodes": [
{
"node_id": "root",
"prompt_text": plan["ivr_root_prompt"],
"prompt_sequence": [
{
"prompt_audio_key": "ivr/demo-language-ru",
"prompt_text": plan["ivr_root_prompt_ru"],
"language": "ru",
},
{
"prompt_audio_key": "ivr/demo-language-kz",
"prompt_text": plan["ivr_root_prompt_kz"],
"language": "kz",
},
],
"is_terminal": False,
"invalid_target_node_id": "root",
"no_input_target_node_id": "root",
"options": [
{"digit": "1", "target_node_id": "voice_start_kz"},
{"digit": "2", "target_node_id": "voice_start_ru"},
],
},
{
"node_id": "voice_start_ru",
"prompt_text": "Передаем звонок в русскоязычный стартовый voice-сценарий.",
"is_terminal": True,
"outcome_code": "voice_start_ru",
"resolved_queue_id": voice_start_ru_queue_id,
"resolved_queue_code": "voice_start_ru",
"options": [],
},
{
"node_id": "voice_start_kz",
"prompt_text": "Қоңырауды қазақ тіліндегі бастапқы voice-сценарийге өткізіп жатырмыз.",
"is_terminal": True,
"outcome_code": "voice_start_kz",
"resolved_queue_id": voice_start_kz_queue_id,
"resolved_queue_code": "voice_start_kz",
"options": [],
},
]
}
def _raise_for_status(response: httpx.Response, action: str) -> None:
try:
response.raise_for_status()
except httpx.HTTPStatusError as exc:
detail = response.text.strip()
raise RuntimeError(f"{action} failed: {exc.response.status_code} {detail}") from exc
def _build_demo_wav(target_path: Path, duration_seconds: int = 1) -> Path:
target_path.parent.mkdir(parents=True, exist_ok=True)
sample_rate = 8000
amplitude = 12000
frequency = 440.0
total_frames = sample_rate * duration_seconds
with wave.open(str(target_path), "wb") as handle:
handle.setnchannels(1)
handle.setsampwidth(2)
handle.setframerate(sample_rate)
frames = bytearray()
for index in range(total_frames):
sample = int(amplitude * math.sin(2 * math.pi * frequency * (index / sample_rate)))
frames.extend(struct.pack("<h", sample))
handle.writeframes(bytes(frames))
return target_path
def _seed_data_dir() -> Path:
raw = os.getenv("CC_DATA_DIR")
if raw:
return Path(raw).resolve()
local_dir = (ROOT / ".data_local").resolve()
if local_dir.exists():
return local_dir
return (ROOT / ".data").resolve()
def seed_demo(base_url: str) -> dict[str, Any]:
admin = {"X-User": "admin", "X-Role": "admin"}
supervisor = {"X-User": "supervisor", "X-Role": "supervisor"}
operator = {"X-User": "operator", "X-Role": "operator"}
analyst = {"X-User": "analyst", "X-Role": "analyst"}
tag = now_stamp()
plan = build_seed_plan(tag)
sample_audio_path = _build_demo_wav((_seed_data_dir() / "demo_assets" / plan["recording_file_name"]).resolve())
with httpx.Client(base_url=base_url, timeout=10) as client:
health = client.get("/health")
_raise_for_status(health, "gateway health check")
queue = client.post(
"/proxy/routing/queues",
headers=admin,
json={
"name": plan["queue_name"],
"description": "Prepared automatically for a management demo",
"rules": [
{"channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 30},
{"channel": "telegram", "priority": 3, "strategy": "round_robin", "sla_seconds": 45},
{"channel": "webchat", "priority": 3, "strategy": "round_robin", "sla_seconds": 40},
{"channel": "email", "priority": 3, "strategy": "round_robin", "sla_seconds": 120},
],
},
)
_raise_for_status(queue, "queue create")
queue_id = queue.json()["queue_id"]
sales_queue = client.post(
"/proxy/routing/queues",
headers=admin,
json={
"name": "Voice Start KZ Queue",
"description": "Prepared automatically for voice-start routing demo",
"rules": [
{"channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 25},
],
},
)
_raise_for_status(sales_queue, "voice_start_kz queue create")
sales_queue_id = sales_queue.json()["queue_id"]
support_queue = client.post(
"/proxy/routing/queues",
headers=admin,
json={
"name": "Voice Start RU Queue",
"description": "Prepared automatically for voice-start routing demo",
"rules": [
{"channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 35},
],
},
)
_raise_for_status(support_queue, "voice_start_ru queue create")
support_queue_id = support_queue.json()["queue_id"]
customer = client.post(
"/proxy/customer/customers",
json={
"display_name": plan["customer_name"],
"phones": [plan["customer_phone"]],
"preferred_phone": plan["customer_phone"],
"tags": ["demo", "showcase"],
},
)
_raise_for_status(customer, "customer create")
customer_id = customer.json()["customer_id"]
voice_interaction = client.post(
"/proxy/interaction/interactions",
headers=operator,
json={
"channel": "voice",
"subject": plan["voice_subject"],
"customer_id": customer_id,
"queue_id": queue_id,
"priority": 3,
},
)
_raise_for_status(voice_interaction, "voice interaction create")
voice_interaction_id = voice_interaction.json()["interaction_id"]
assign = client.patch(
f"/proxy/interaction/interactions/{voice_interaction_id}/assign",
headers=supervisor,
json={"assignee": "operator_a"},
)
_raise_for_status(assign, "voice interaction assign")
escalate = client.post(
f"/proxy/interaction/interactions/{voice_interaction_id}/escalate",
headers=operator,
json={"target_queue_id": "line2"},
)
_raise_for_status(escalate, "voice interaction escalate")
close = client.patch(
f"/proxy/interaction/interactions/{voice_interaction_id}/status",
headers=operator,
json={"status": "closed"},
)
_raise_for_status(close, "voice interaction close")
active_interaction = client.post(
"/proxy/interaction/interactions",
headers=operator,
json={
"channel": "telegram",
"subject": plan["chat_subject"],
"customer_id": customer_id,
"queue_id": "line2",
"priority": 3,
},
)
_raise_for_status(active_interaction, "active interaction create")
active_interaction_id = active_interaction.json()["interaction_id"]
voice_event = client.post(
"/proxy/voice/integrations/voice/events",
headers=operator,
json={
"event_type": plan["voice_event_type"],
"call_id": "demo_call",
"interaction_id": voice_interaction_id,
"payload": {"source": "demo-seed"},
},
)
_raise_for_status(voice_event, "voice event send")
voice_event_id = voice_event.json()["event_id"]
recording_ready_event = client.post(
"/proxy/voice/integrations/voice/events",
headers=operator,
json={
"event_type": "recording.ready",
"call_id": "demo_call",
"interaction_id": voice_interaction_id,
"payload": {
"source": "demo-seed",
"source_path": str(sample_audio_path),
"file_name": plan["recording_file_name"],
"mime_type": "audio/wav",
"duration_seconds": 1,
"recorded_at": utc_now().replace(microsecond=0).isoformat(),
},
},
)
_raise_for_status(recording_ready_event, "recording ready event send")
recording_event_id = recording_ready_event.json()["event_id"]
recording = client.post(
f"/proxy/recording/recordings/import-from-voice-event/{recording_event_id}",
headers=supervisor,
)
_raise_for_status(recording, "recording import")
recording_id = recording.json()["recording_id"]
ivr_flow = client.post(
"/proxy/ivr/ivr/flows",
headers=admin,
json={
"name": "Demo IVR Flow",
"description": "Prepared automatically for a management demo",
"queue_id": queue_id,
"entry_node_id": "root",
"flow_json": _demo_ivr_flow_document(
plan,
voice_start_kz_queue_id=sales_queue_id,
voice_start_ru_queue_id=support_queue_id,
),
"is_active": True,
},
)
_raise_for_status(ivr_flow, "ivr flow create")
ivr_flow_id = ivr_flow.json()["flow_id"]
ivr_session_start = client.post(
"/proxy/ivr/ivr/sessions/start",
headers=admin,
json={
"call_id": "demo_call_ivr",
"queue_id": queue_id,
"interaction_id": voice_interaction_id,
},
)
_raise_for_status(ivr_session_start, "ivr session start")
ivr_session_id = ivr_session_start.json()["session"]["session_id"]
ivr_session_step = client.post(
f"/proxy/ivr/ivr/sessions/{ivr_session_id}/dtmf",
headers=admin,
json={"digit": "1"},
)
_raise_for_status(ivr_session_step, "ivr language step")
ivr_session_step = client.post(
f"/proxy/ivr/ivr/sessions/{ivr_session_id}/dtmf",
headers=admin,
json={"digit": "2"},
)
_raise_for_status(ivr_session_step, "ivr dtmf step")
ivr_session_payload = ivr_session_step.json()
ivr_route_preview = client.post(
f"/proxy/routing/queues/{queue_id}/route?channel=voice&priority=3&ivr_session_id={ivr_session_id}",
headers=admin,
)
_raise_for_status(ivr_route_preview, "ivr route preview")
telegram = client.post(
"/proxy/telegram/integrations/telegram/webhook",
json={
"chat_id": "demo_chat",
"text": plan["telegram_text"],
"customer_external_id": customer_id,
"payload": {"source": "demo-seed"},
},
)
_raise_for_status(telegram, "telegram webhook send")
telegram_id = telegram.json()["message_id"]
webchat = client.post(
"/proxy/webchat/integrations/webchat/messages",
json={
"session_id": "demo_webchat_session",
"text": plan["webchat_text"],
"visitor_name": "Demo Visitor",
"customer_external_id": customer_id,
"queue_id": "line2",
"priority": 3,
"payload": {"source": "demo-seed", "subject": "Demo webchat intake"},
},
)
_raise_for_status(webchat, "webchat message send")
webchat_id = webchat.json()["message_id"]
webchat_interaction_id = webchat.json()["interaction_id"]
email = client.post(
"/proxy/email/integrations/email/messages",
json={
"from_email": "demo.user@example.com",
"subject": plan["email_subject"],
"body": plan["email_body"],
"customer_external_id": customer_id,
"queue_id": "line2",
"priority": 3,
"payload": {"source": "demo-seed", "mailbox": "support@example.com"},
},
)
_raise_for_status(email, "email message send")
email_id = email.json()["message_id"]
email_interaction_id = email.json()["interaction_id"]
category = client.post(
"/proxy/kb/knowledge/categories",
headers=analyst,
json={"name": plan["kb_category"], "description": "Demo search content"},
)
_raise_for_status(category, "kb category create")
category_id = category.json()["category_id"]
article = client.post(
"/proxy/kb/knowledge/articles",
headers=analyst,
json={
"category_id": category_id,
"title": plan["kb_title"],
"body": "Use the demo-showcase article when management asks about answer prompts.",
"tags": ["demo", plan["kb_keyword"]],
},
)
_raise_for_status(article, "kb article create")
article_id = article.json()["article_id"]
search = client.get(f"/proxy/kb/knowledge/search?q={plan['kb_keyword']}")
_raise_for_status(search, "kb search")
agent1 = client.post(
"/proxy/supervisor/supervisor/agent-states",
headers=supervisor,
json={"agent_id": "demo_agent_a", "state": "READY", "queue_id": "line2"},
)
_raise_for_status(agent1, "supervisor agent update 1")
agent2 = client.post(
"/proxy/supervisor/supervisor/agent-states",
headers=supervisor,
json={"agent_id": "demo_agent_b", "state": "BUSY", "queue_id": "line2"},
)
_raise_for_status(agent2, "supervisor agent update 2")
queue_metrics = client.post(
"/proxy/supervisor/supervisor/queue-metrics?queue_id=line2&in_queue=1&avg_wait_seconds=14",
headers=supervisor,
)
_raise_for_status(queue_metrics, "supervisor queue metrics")
realtime = client.get("/proxy/supervisor/supervisor/realtime", headers=supervisor)
_raise_for_status(realtime, "supervisor realtime")
kpi_rows = [
{
"queue_id": "line2",
"channel": "voice",
"agent_id": "demo_agent_b",
"answered": True,
"wait_seconds": 15,
"handle_seconds": 90,
"abandoned": False,
"resolved_first_contact": True,
},
{
"queue_id": "line2",
"channel": "webchat",
"agent_id": "demo_agent_a",
"answered": True,
"wait_seconds": 25,
"handle_seconds": 110,
"abandoned": False,
"resolved_first_contact": False,
},
{
"queue_id": "line2",
"channel": "email",
"agent_id": None,
"answered": False,
"wait_seconds": 12,
"handle_seconds": 0,
"abandoned": True,
"resolved_first_contact": False,
},
]
for idx, row in enumerate(kpi_rows, start=1):
created = client.post("/proxy/reporting/reports/events", json=row)
_raise_for_status(created, f"kpi event ingest {idx}")
kpi = client.get("/proxy/reporting/reports/kpi?queue_id=line2&sl_threshold_seconds=30")
_raise_for_status(kpi, "kpi query")
event_bus_enabled = os.getenv("EVENT_BUS_ENABLED", "0").strip().lower() in {"1", "true", "yes", "on"}
event_bus_smoke_passed = False
seeded_event_id = None
if event_bus_enabled:
try:
from scripts.event_bus_smoke import run_smoke_check
smoke = run_smoke_check(base_url=base_url, auth_mode="legacy_headers", timeout_seconds=10)
event_bus_smoke_passed = bool(smoke.get("passed"))
seeded_event_id = smoke.get("event_id")
except Exception:
event_bus_smoke_passed = False
return {
"generated_at": utc_now().isoformat(),
"base_url": base_url,
"demo_login": {"username": "admin", "password": "admin123"},
"queue_id": queue_id,
"customer_id": customer_id,
"voice_interaction_id": voice_interaction_id,
"active_interaction_id": active_interaction_id,
"voice_event_id": voice_event_id,
"recording_event_id": recording_event_id,
"recording_id": recording_id,
"recording_call_id": "demo_call",
"recording_file_name": plan["recording_file_name"],
"event_bus_enabled": event_bus_enabled,
"event_bus_smoke_passed": event_bus_smoke_passed,
"seeded_event_id": seeded_event_id,
"ivr_flow_id": ivr_flow_id,
"ivr_session_id": ivr_session_id,
"ivr_outcome_code": ivr_session_payload["session"]["outcome_code"],
"ivr_resolved_queue_id": ivr_session_payload["session"]["resolved_queue_id"],
"ivr_route_preview": ivr_route_preview.json(),
"telegram_message_id": telegram_id,
"webchat_message_id": webchat_id,
"webchat_interaction_id": webchat_interaction_id,
"email_message_id": email_id,
"email_interaction_id": email_interaction_id,
"kb_category_id": category_id,
"kb_article_id": article_id,
"kb_keyword": plan["kb_keyword"],
"sample_recording_path": str(sample_audio_path),
"kpi_snapshot": kpi.json(),
"supervisor_snapshot": realtime.json(),
}
def main() -> int:
parser = argparse.ArgumentParser(description="Seed demo-ready data into the local MVP stack")
parser.add_argument("--base-url", default="http://localhost:8080", help="Gateway base URL")
parser.add_argument(
"--output",
default=str(DEFAULT_OUTPUT),
help="Where to store the seed summary JSON",
)
args = parser.parse_args()
output_path = Path(args.output)
if not output_path.is_absolute():
output_path = (ROOT / output_path).resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
summary = seed_demo(args.base_url)
output_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
print("Demo data is ready.")
print(f"UI: {args.base_url}/operator")
print(f"Summary: {output_path}")
print(f"Customer: {summary['customer_id']}")
print(f"Closed interaction: {summary['voice_interaction_id']}")
print(f"Active interaction: {summary['active_interaction_id']}")
print(f"Recording: {summary['recording_id']}")
print(f"IVR flow: {summary['ivr_flow_id']}")
print(f"IVR session: {summary['ivr_session_id']}")
print(f"Webchat interaction: {summary['webchat_interaction_id']}")
print(f"Email interaction: {summary['email_interaction_id']}")
print(f"KB keyword: {summary['kb_keyword']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())