Files
call-center/scripts/demo_seed.py
T

606 lines
24 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], *, sales_queue_id: str, support_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-kz",
"prompt_text": plan["ivr_root_prompt_kz"],
"language": "kz",
},
{
"prompt_audio_key": "ivr/demo-language-ru",
"prompt_text": plan["ivr_root_prompt_ru"],
"language": "ru",
},
],
"is_terminal": False,
"invalid_target_node_id": "root",
"no_input_target_node_id": "root",
"options": [
{"digit": "1", "target_node_id": "menu_kz"},
{"digit": "2", "target_node_id": "menu_ru"},
],
},
{
"node_id": "menu_ru",
"prompt_text": plan["ivr_ru_menu_prompt"],
"prompt_audio_key": "ivr/demo-menu-ru",
"is_terminal": False,
"invalid_target_node_id": "menu_ru",
"no_input_target_node_id": "menu_ru",
"options": [
{"digit": "1", "target_node_id": "sales_ru"},
{"digit": "2", "target_node_id": "support_ru"},
],
},
{
"node_id": "menu_kz",
"prompt_text": plan["ivr_kz_menu_prompt"],
"prompt_audio_key": "ivr/demo-menu-kz",
"is_terminal": False,
"invalid_target_node_id": "menu_kz",
"no_input_target_node_id": "menu_kz",
"options": [
{"digit": "1", "target_node_id": "sales_kz"},
{"digit": "2", "target_node_id": "support_kz"},
],
},
{
"node_id": "sales_ru",
"prompt_text": "Переводим в отдел продаж.",
"prompt_audio_key": "ivr/demo-sales-ru",
"is_terminal": True,
"outcome_code": "sales_route_ru",
"resolved_queue_id": sales_queue_id,
"resolved_queue_code": "ivr_sales_ai_ru",
"options": [],
},
{
"node_id": "support_ru",
"prompt_text": "Переводим в службу поддержки.",
"prompt_audio_key": "ivr/demo-support-ru",
"is_terminal": True,
"outcome_code": "support_route_ru",
"resolved_queue_id": support_queue_id,
"resolved_queue_code": "ivr_support_ai_ru",
"options": [],
},
{
"node_id": "sales_kz",
"prompt_text": "Сату бөліміне қосып жатырмыз.",
"prompt_audio_key": "ivr/demo-sales-kz",
"is_terminal": True,
"outcome_code": "sales_route_kz",
"resolved_queue_id": sales_queue_id,
"resolved_queue_code": "ivr_sales_ai_kz",
"options": [],
},
{
"node_id": "support_kz",
"prompt_text": "Қолдау қызметіне қосып жатырмыз.",
"prompt_audio_key": "ivr/demo-support-kz",
"is_terminal": True,
"outcome_code": "support_route_kz",
"resolved_queue_id": support_queue_id,
"resolved_queue_code": "ivr_support_ai_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": "IVR Sales Queue",
"description": "Prepared automatically for IVR routing demo",
"rules": [
{"channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 25},
],
},
)
_raise_for_status(sales_queue, "sales queue create")
sales_queue_id = sales_queue.json()["queue_id"]
support_queue = client.post(
"/proxy/routing/queues",
headers=admin,
json={
"name": "IVR Support Queue",
"description": "Prepared automatically for IVR routing demo",
"rules": [
{"channel": "voice", "priority": 3, "strategy": "round_robin", "sla_seconds": 35},
],
},
)
_raise_for_status(support_queue, "support 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,
sales_queue_id=sales_queue_id,
support_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())