TTS reads raw text digit-by-digit with no number/date normalization layer. Short numbers like 1414 were read as a single 4-digit number (tysyacha chetyresta chetyrnadtsat) instead of a spoken code, and dates like '25 chisla' were read in the wrong grammatical case (dvadtsat pyat chislo instead of dvadtsat pyatogo chisla). Extend the voice delivery_hint with explicit spell-out rules so the model itself produces already-correct spoken-form text.
178 lines
8.7 KiB
Python
178 lines
8.7 KiB
Python
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
from typing import Any
|
||
|
||
|
||
_IDENTITY_PATTERNS: tuple[str, ...] = (
|
||
"кто ты",
|
||
"кто вы",
|
||
"ты кто",
|
||
"вы кто",
|
||
"как тебя зовут",
|
||
"как вас зовут",
|
||
"твое имя",
|
||
"твоё имя",
|
||
"ваше имя",
|
||
"представься",
|
||
"скажи кто ты",
|
||
"скажи мне кто ты",
|
||
"робот ты",
|
||
"ты робот",
|
||
"ты бот",
|
||
"ты ии",
|
||
"ты искусственный интеллект",
|
||
"ты оператор",
|
||
"вы оператор",
|
||
"who are you",
|
||
"what is your name",
|
||
"сен кімсің",
|
||
"сіз кімсіз",
|
||
"атың кім",
|
||
"атыңыз кім",
|
||
)
|
||
|
||
|
||
def _config_value(config: Any | None, name: str, default: str) -> str:
|
||
if config is None:
|
||
return default
|
||
value = getattr(config, name, None)
|
||
if value is None and isinstance(config, dict):
|
||
value = config.get(name)
|
||
normalized = str(value or "").strip()
|
||
return normalized or default
|
||
|
||
|
||
def _agent_name(config: Any | None) -> str:
|
||
return _config_value(config, "agent_name", "Айнур")
|
||
|
||
|
||
def _company_name(config: Any | None) -> str:
|
||
return _config_value(config, "company_name", "DigiOps")
|
||
|
||
|
||
def _format_config_text(text: str, config: Any | None) -> str:
|
||
return (
|
||
str(text or "")
|
||
.replace("{agent_name}", _agent_name(config))
|
||
.replace("{company_name}", _company_name(config))
|
||
.strip()
|
||
)
|
||
|
||
|
||
def is_identity_request(text: str) -> bool:
|
||
normalized = str(text or "").strip().lower().replace("ё", "е")
|
||
if not normalized:
|
||
return False
|
||
normalized = re.sub(r"[^\w\sәіңғүұқөһ-]+", " ", normalized, flags=re.UNICODE)
|
||
compact = " ".join(normalized.split())
|
||
return any(pattern.replace("ё", "е") in compact for pattern in _IDENTITY_PATTERNS)
|
||
|
||
|
||
def customer_persona_mode() -> str:
|
||
return str(os.getenv("AI_CUSTOMER_PERSONA_MODE", "operator_humanlike") or "operator_humanlike").strip().lower()
|
||
|
||
|
||
def disclosure_mode() -> str:
|
||
return str(os.getenv("AI_DISCLOSURE_MODE", "hidden") or "hidden").strip().lower()
|
||
|
||
|
||
def voice_policy_mode() -> str:
|
||
return str(os.getenv("AI_VOICE_POLICY_MODE", "llm_guarded") or "llm_guarded").strip().lower()
|
||
|
||
|
||
def disclosure_hidden() -> bool:
|
||
return disclosure_mode() == "hidden"
|
||
|
||
|
||
def voice_disclosure_prefix(language: str) -> str:
|
||
if disclosure_hidden():
|
||
return ""
|
||
if str(language or "").strip().lower() == "kz":
|
||
return "Men kompaniya atynan jauap berip turmyn. "
|
||
return "Отвечаю от имени линии поддержки компании. "
|
||
|
||
|
||
def identity_reply(language: str, config: Any | None = None) -> str:
|
||
if str(language or "").strip().lower() == "kz":
|
||
default = f"Мен {_agent_name(config)}мын, {_company_name(config)} байланыс орталығының операторымын. Қалай көмектесе аламын?"
|
||
return _format_config_text(_config_value(config, "identity_reply_kz", default), config)
|
||
default = f"Я {_agent_name(config)}, оператор контакт-центра {_company_name(config)}. Чем могу помочь?"
|
||
return _format_config_text(_config_value(config, "identity_reply_ru", default), config)
|
||
|
||
|
||
def voice_greeting(language: str, config: Any | None = None) -> str:
|
||
if str(language or "").strip().lower() == "kz":
|
||
default = f"Сәлеметсіз бе. Мен {_agent_name(config)}мын. Қалай көмектесе аламын?"
|
||
return _format_config_text(_config_value(config, "voice_greeting_kz", default), config)
|
||
default = f"Здравствуйте. Я {_agent_name(config)}. Подскажите, пожалуйста, чем помочь. (тест деплоя)"
|
||
return _format_config_text(_config_value(config, "voice_greeting_ru", default), config)
|
||
|
||
|
||
def voice_handoff_reply(language: str) -> str:
|
||
if str(language or "").strip().lower() == "kz":
|
||
return "Бір сәт, сізді операторға қосамын."
|
||
return "Секунду, соединяю вас с оператором."
|
||
|
||
|
||
def text_resolution_reply(language: str) -> str:
|
||
if str(language or "").strip().lower() == "kz":
|
||
return "Жақсы, белгілеп қоямын. Қажет болса, осы жерден қайта жаза аласыз."
|
||
return "Хорошо, отмечу это. Если понадобится, можно продолжить здесь."
|
||
|
||
|
||
def human_fallback_reply(language: str, *, is_greeting: bool = False) -> str:
|
||
if str(language or "").strip().lower() == "kz":
|
||
if is_greeting:
|
||
return "Сәлеметсіз бе. Сұрағыңызды жазыңыз не айтыңыз, көмектесуге тырысамын."
|
||
return "Түсіндім. Нақтырақ айтып жіберсеңіз, бірден жалғастырамын."
|
||
if is_greeting:
|
||
return "Здравствуйте. Напишите или коротко расскажите, чем помочь."
|
||
return "Понял вас. Уточните, пожалуйста, детальнее, и я сразу продолжу."
|
||
|
||
|
||
def operator_system_prompt(*, language: str, channel_label: str, is_voice: bool, config: Any | None = None) -> str:
|
||
preferred_language = "Kazakh" if str(language or "").strip().lower() == "kz" else "Russian"
|
||
delivery_hint = (
|
||
"The reply will be spoken aloud over a phone call, so keep it concise, natural, and easy to listen to. "
|
||
"Prefer one or two short sentences and at most one clarifying question. "
|
||
"The text-to-speech engine reads exactly what you write, digit by digit, with no number formatting of its own, "
|
||
"so never output bare digits for phone numbers, hotline numbers, or dates — always spell them out in words "
|
||
"the way a person would actually say them aloud in natural spoken Russian/Kazakh. "
|
||
"Short hotline or service numbers (e.g. 1414, 109) must be spelled out the way people say them as a code, "
|
||
"grouped and read naturally (\"1414\" as \"четырнадцать четырнадцать\", not \"тысяча четыреста четырнадцать\"). "
|
||
"Calendar dates must use the correct spoken grammatical case (\"25 числа\" as \"двадцать пятого числа\", "
|
||
"not \"двадцать пять число\"; \"14 марта\" as \"четырнадцатого марта\")."
|
||
if is_voice
|
||
else "The reply should read like a concise message from a live first-line operator."
|
||
)
|
||
default_base_prompt = (
|
||
f"Ты {_agent_name(config)}, единый оператор контакт-центра {_company_name(config)} для звонков, "
|
||
"Telegram и других каналов. Всегда сохраняй одну и ту же личность. Когда говоришь о себе, "
|
||
"используй женский род. Не завершай диалог самостоятельно."
|
||
)
|
||
base_prompt = _format_config_text(_config_value(config, "base_system_prompt", default_base_prompt), config)
|
||
identity_instruction = (
|
||
f"If the customer asks who you are or what your name is, answer as {_agent_name(config)}. "
|
||
f"For {preferred_language}, use this identity reply when appropriate: "
|
||
f"{identity_reply(language, config)}"
|
||
)
|
||
return (
|
||
f"{base_prompt}\n\n"
|
||
f"You are a first-line company operator handling customer conversations in {channel_label}. "
|
||
f"Reply in {preferred_language}. Speak naturally, warmly, and confidently like a human operator. "
|
||
f"Your name is {_agent_name(config)} and you must not introduce yourself with any other name. "
|
||
f"{identity_instruction} "
|
||
"Do not mention a knowledge base, snippets, internal notes, policies, or hidden tools. "
|
||
"Use the provided business context, conversation history, and KB snippets only as factual sources. "
|
||
"Paraphrase them naturally instead of quoting them verbatim. "
|
||
"Never invent order statuses, tariffs, discounts, deadlines, addresses, availability, approvals, or actions "
|
||
"that are not supported by context. If the available facts are insufficient, ask one short clarifying question. "
|
||
"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, extracted_name, confidence, needs_handoff, "
|
||
"handoff_reason, case_action, kb_refs. case_action must be one of none, close, escalate, keep_open."
|
||
)
|