feat: enhance voice reply logic to prevent duplicate name addressing and improve greeting handling
deploy / deploy (push) Successful in 32s

This commit is contained in:
2026-08-30 11:53:56 +05:00
parent 0a829d26c7
commit fe9b3f3a80
2 changed files with 108 additions and 5 deletions
+53 -5
View File
@@ -603,20 +603,60 @@ def _voice_has_name_correction(text: str | None) -> bool:
return any(marker in normalized for marker in correction_markers)
def _voice_reply_with_name(language: str, reply_text: str, name: str | None) -> str:
def _voice_reply_already_names_customer(reply_text: str, normalized_short: str) -> bool:
"""Whether `reply_text` already addresses the customer by name.
Russian and Kazakh names decline grammatically (e.g. "Данияр" -> "Данияре",
"Данияру", "Данияра"), so the model's own reply often names the customer in
an inflected form that never exact-matches `normalized_short`. An exact
token match alone misses that and this function then goes on to prepend
the name a second time, producing a literal "Данияр, Здравствуйте, Данияре!"
duplicate. Matching on a stem prefix instead of the full token catches any
declined form of the same name.
"""
tokens = _voice_text_key(reply_text).split(" ")
if normalized_short in tokens:
return True
stem_len = max(len(normalized_short) - 2, 3)
stem = normalized_short[:stem_len]
return any(len(token) >= stem_len and token.startswith(stem) for token in tokens)
def _voice_greeting_word(language: str) -> str:
if str(language or "").strip().lower() == "kz":
return "Сәлеметсіз бе"
return "Здравствуйте"
def _voice_reply_with_name(
language: str,
reply_text: str,
name: str | None,
*,
greet: bool = False,
) -> str:
"""Prefix `reply_text` with the customer's name.
The prefix is always one of exactly two deterministic forms — "{name}, ..."
or "Здравствуйте, {name}, ..." (`greet=True`, used the turn their name is
first learned) — never left to the model's own free-form phrasing. That is
what keeps this from colliding with a self-introduced name inside
`reply_text` in the first place (see `_voice_reply_already_names_customer`).
"""
short_name = _voice_short_name(name)
if not short_name:
return reply_text
prefix = _voice_disclosure_prefix(language)
normalized_short = _voice_text_key(short_name)
lead = f"{_voice_greeting_word(language)}, {short_name}" if greet else short_name
if reply_text.startswith(prefix):
rest = reply_text[len(prefix) :].lstrip()
if normalized_short in _voice_text_key(rest).split(" "):
if _voice_reply_already_names_customer(rest, normalized_short):
return reply_text
return f"{prefix}{short_name}, {rest}"
if normalized_short in _voice_text_key(reply_text).split(" "):
return f"{prefix}{lead}, {rest}"
if _voice_reply_already_names_customer(reply_text, normalized_short):
return reply_text
return f"{short_name}, {reply_text}"
return f"{lead}, {reply_text}"
def _voice_name_metadata(
@@ -1613,6 +1653,12 @@ def _voice_llm_prompt_messages(
is_voice=True,
config=operator_config,
)
system_prompt += (
" Do not open `reply_text` with a greeting or by addressing the customer by name "
"(e.g. do not write 'Здравствуйте, <имя>' or start with '<имя>,'). The system inserts "
"the customer's name into the spoken reply separately, so naming them yourself would "
"make it get said twice."
)
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, "
@@ -2500,10 +2546,12 @@ def turn_voice_session(session_id: str, payload: VoiceAITurnIn) -> VoiceAITurnDe
decision_metadata.update(decision.get("metadata") or {})
suppress_name_prefix = bool(request_metadata.get("suppress_name_prefix")) if isinstance(request_metadata, dict) else False
if effective_name_status == "name_obtained" and effective_name_value and not early_plan_only and not suppress_name_prefix:
just_learned_name = current_name_status != "name_obtained"
decision["reply_text"] = _voice_reply_with_name(
decision["language"],
decision["reply_text"],
effective_name_value,
greet=just_learned_name,
)
elif inline_name_followup and not decision["needs_handoff"] and not early_plan_only:
inline_followup = _voice_inline_name_followup(decision["language"], config)