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.
122 lines
5.2 KiB
Python
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())
|