Files
call-center/services/shared/ai_operator_config.py
T
arys 82c89eff5a feat: give aimaq voice AI a Qazaqgaz Aimaq persona (Zhanna) and language-choice greeting
Parametrize ai_operator_default_config() via AI_OPERATOR_* env vars,
falling back to the existing hardcoded defaults so the other two stacks
(call-center, sales-call-center) that share this code are unaffected.

Set aimaq-only overrides matching the client-provided script (Скрипт
ии-оператора КГА.docx): agent renamed to Жанна, company to Казакгаз
Аймак, the voice greeting now asks the caller whether Russian or
Kazakh is more convenient before anything else, and the base system
prompt instructs the model to commit to whichever language the caller
picks for the rest of the call, ask how to address them, and confirm
there's nothing else before saying goodbye.
2026-08-30 12:22:00 +05:00

122 lines
5.2 KiB
Python

from __future__ import annotations
import json
import os
from typing import Any
from sqlalchemy import select
from services.shared.core import utc_now_iso
from services.shared.models import AIOperatorConfig, AIOperatorConfigOut
from services.shared.sql_models import AIOperatorSettingsRow
AI_OPERATOR_SETTINGS_KEY = "global"
def _env_field(name: str, default: str) -> str:
value = os.getenv(name)
if value is None:
return default
normalized = value.strip()
return normalized or default
def ai_operator_default_config() -> AIOperatorConfig:
agent_name = _env_field("AI_OPERATOR_AGENT_NAME", "Айнур")
company_name = _env_field("AI_OPERATOR_COMPANY_NAME", "DigiOps")
return AIOperatorConfig(
agent_name=agent_name,
company_name=company_name,
base_system_prompt=_env_field(
"AI_OPERATOR_BASE_SYSTEM_PROMPT",
(
f"Ты {agent_name}, единый ИИ-оператор контакт-центра {company_name} для звонков, Telegram и других "
f"каналов. Всегда сохраняй одну и ту же личность: тебя зовут {agent_name}. Если клиент спрашивает, "
f"кто ты или как тебя зовут, отвечай, что ты {agent_name}. Отвечай естественно, кратко и по делу. "
"Когда говоришь о себе, используй женский род: могла, смогла, сделала, готова, проверила, нашла. "
"Не завершай диалог самостоятельно и говори «до свидания» только если клиент явно попрощался или "
"попросил завершить разговор. Телефонные номера читай по цифрам. Не используй Markdown, URL или "
"таблицы."
),
),
identity_reply_ru=_env_field(
"AI_OPERATOR_IDENTITY_REPLY_RU",
f{agent_name}, оператор контакт-центра {company_name}. Чем могу помочь?",
),
identity_reply_kz=_env_field(
"AI_OPERATOR_IDENTITY_REPLY_KZ",
f"Мен {agent_name}мын, {company_name} байланыс орталығының операторымын. Қалай көмектесе аламын?",
),
voice_greeting_ru=_env_field(
"AI_OPERATOR_VOICE_GREETING_RU",
f"Здравствуйте. Я {agent_name}. Подскажите, пожалуйста, чем помочь. (тест деплоя)",
),
voice_greeting_kz=_env_field(
"AI_OPERATOR_VOICE_GREETING_KZ",
f"Сәлеметсіз бе. Мен {agent_name}мын. Қалай көмектесе аламын?",
),
)
def _settings_row(session) -> AIOperatorSettingsRow | None:
return session.execute(
select(AIOperatorSettingsRow).where(AIOperatorSettingsRow.settings_key == AI_OPERATOR_SETTINGS_KEY)
).scalar_one_or_none()
def _normalize_config_payload(raw: Any) -> AIOperatorConfig:
if isinstance(raw, AIOperatorConfig):
return raw
if not isinstance(raw, dict):
raise ValueError("AI operator config payload must be an object")
return AIOperatorConfig.model_validate(raw)
def load_ai_operator_config(session) -> AIOperatorConfigOut:
default_config = ai_operator_default_config()
row = _settings_row(session)
if row is None:
return AIOperatorConfigOut(config=default_config, updated_at=None, source="defaults")
try:
parsed = json.loads(row.config_json or "{}")
config = _normalize_config_payload(parsed)
return AIOperatorConfigOut(config=config, updated_at=row.updated_at, source="database")
except Exception:
return AIOperatorConfigOut(config=default_config, updated_at=row.updated_at, source="defaults")
def load_effective_ai_operator_config(session) -> AIOperatorConfig:
return load_ai_operator_config(session).config
def save_ai_operator_config(
session,
config_payload: AIOperatorConfig | dict[str, Any],
) -> AIOperatorConfigOut:
config = _normalize_config_payload(config_payload)
row = _settings_row(session)
now = utc_now_iso()
if row is None:
row = AIOperatorSettingsRow(
settings_key=AI_OPERATOR_SETTINGS_KEY,
config_json="{}",
updated_at=now,
)
session.add(row)
row.config_json = json.dumps(config.model_dump(), ensure_ascii=False)
row.updated_at = now
session.commit()
session.refresh(row)
return AIOperatorConfigOut(config=config, updated_at=row.updated_at, source="database")
def sync_ai_operator_config_from_code(session) -> AIOperatorConfigOut:
"""Overwrite the DB-stored config with the code defaults.
Called on service startup so `ai_operator_default_config()` is the
source of truth: edit it, deploy, and the DB row is overwritten to match.
Any manual edits made via the API/psql since the last deploy are lost.
"""
return save_ai_operator_config(session, ai_operator_default_config())