fix: bring aimaq host-override files (voice.py, persona env) back into git

Both services/ai_orchestrator_service/voice.py and the aimaq persona/DOMAIN
SCOPE prompt were bind-mounted straight from the host on the aimaq stack,
bypassing git and CI entirely since they were first hand-edited in prod.

voice.py: merged the host's live business logic (gas/aimaq domain keyword
list, off-domain Kazakh/Russian replies, disabled re-correction of an already
obtained name) with the timeout_seconds fix from 09bcf74 that never reached
aimaq because the bind mount blocked it.

aimaq.env.production: replaced the AI_OPERATOR_* env values (which were never
interpolated -- {agent_name}/{company_name} would have been read literally)
with the final resolved Zhanna/Kazakgaz Aimaq text including the DOMAIN SCOPE
clause, matching what was actually live on the host.
This commit is contained in:
2026-08-30 07:45:35 +00:00
parent ff85fa27fb
commit 9d918af12a
2 changed files with 84 additions and 38 deletions
+79 -33
View File
@@ -604,16 +604,6 @@ def _voice_has_name_correction(text: str | None) -> bool:
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
@@ -635,14 +625,6 @@ def _voice_reply_with_name(
*,
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
@@ -786,13 +768,7 @@ def _voice_downstream_name_update(
action = "provide" if not name_value else "confirm"
candidate = candidate or name_value
elif status == "name_obtained":
if (
candidate
and candidate_key
and candidate_key != current_key
and _voice_has_name_correction(transcript_text)
):
action = "correct"
pass
else:
if explicit_candidate:
action = "provide"
@@ -1079,7 +1055,6 @@ def _voice_has_service_topic(text: str | None) -> bool:
"ошибк",
"проблем",
"сбой",
"интернет",
"связь",
"оператор",
"менеджер",
@@ -1087,6 +1062,40 @@ def _voice_has_service_topic(text: str | None) -> bool:
"компан",
"подключ",
"доставк",
"газ",
"счётчик",
"счетчик",
"ваучер",
"отключ",
"квитанц",
"показан",
"приложен",
"безопасн",
"техническ",
"плит",
"котл",
"труб",
"утечк",
"запах",
"абонент",
"договор",
"поверк",
"монтаж",
"счёт",
"счет",
"долг",
"задолжен",
"перерасчёт",
"перерасчет",
"регион",
"аимак",
"aimaq",
"qazaqgaz",
"казахгаз",
"есептегіш",
"төлеу",
"өтінім",
"шарт",
)
return any(marker in normalized for marker in service_markers)
@@ -1120,6 +1129,37 @@ def _voice_is_off_domain_request(text: str | None) -> bool:
"java",
"javascript",
"погод",
"плов",
"приготов",
"кулинар",
"блюдо",
"фильм",
"кино",
"спорт",
"футбол",
"хоккей",
"теннис",
"баскетбол",
"президент",
"политик",
"выборы",
"правительств",
"курс валют",
"криптовалют",
"биткоин",
"песн",
"музык",
"танц",
"шутк",
"загадк",
"сериал",
"книга",
"стихотвор",
"чемпионат",
"ауа райы",
"аспаздық",
"кітап",
"ән айт",
)
if any(marker in normalized for marker in broad_markers):
return True
@@ -1129,7 +1169,14 @@ def _voice_is_off_domain_request(text: str | None) -> bool:
"что такое",
"объясни",
"расскажи про",
"почему",
"посоветуй",
"как приготовить",
"какая погода",
"кто президент",
"кто выиграл",
"какая команда",
"ауа райы қалай",
"әнді айт",
)
return any(normalized.startswith(prefix) for prefix in broad_openers)
@@ -1137,13 +1184,12 @@ def _voice_is_off_domain_request(text: str | None) -> bool:
def _voice_off_domain_reply(language: str) -> tuple[str, str]:
if language == "kz":
return (
"Men kompaniyamyzdyn qyzmetteri men otinishteri boiynsha komek bere alamyn. "
"Eger suraq bizdin qyzmetke qatysty bolsa, qysqasha naqtylaңыз. Qalasaңыз, operatorga qosamyn.",
"AI qongyraudyn taqyrybyn kompaniya qyzmetteri sheginde naqtylaudy usyndy.",
"Кешіріңіз, мен тек газ қызметтері бойынша көмектесемін: төлеу, есептегіш, ваучер. Қалай көмектесе аламын?",
"AI off-topic сұрауды газ қызметтері тақырыбына шектеді.",
)
return (
"Я помогу по вопросам наших услуг и обращений. Если вопрос связан с нашей компанией, скажите коротко, что именно нужно. Если хотите, сразу соединю с оператором.",
"AI мягко вернул разговор к вопросам компании и предложил перевод на оператора.",
"Извините, я помогаю только по вопросам газа: оплата, счётчики, ваучеры. Чем могу помочь?",
"AI отклонил off-topic вопрос и ограничил тему газоснабжением.",
)
@@ -1856,7 +1902,7 @@ def _voice_decision(
"latency_ms": 1,
}
if not kb_results and _voice_is_off_domain_request(normalized):
if _voice_is_off_domain_request(normalized):
reply_text, summary_text = _voice_off_domain_reply(language)
decision = {
"language": language,