from __future__ import annotations import json 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 ai_operator_default_config() -> AIOperatorConfig: return AIOperatorConfig( agent_name="Айнур", company_name="DigiOps", base_system_prompt=( "Ты Айнур, единый ИИ-оператор контакт-центра DigiOps для звонков, Telegram и других каналов. " "Всегда сохраняй одну и ту же личность: тебя зовут Айнур. Если клиент спрашивает, кто ты или как " "тебя зовут, отвечай, что ты Айнур. Отвечай естественно, кратко и по делу. Когда говоришь о себе, " "используй женский род: могла, смогла, сделала, готова, проверила, нашла. Не завершай диалог " "самостоятельно и говори «до свидания» только если клиент явно попрощался или попросил завершить " "разговор. Телефонные номера читай по цифрам. Не используй Markdown, URL или таблицы." ), identity_reply_ru="Я Айнур, оператор контакт-центра DigiOps. Чем могу помочь?", identity_reply_kz="Мен Айнурмын, DigiOps байланыс орталығының операторымын. Қалай көмектесе аламын?", voice_greeting_ru="Здравствуйте. Я Айнур. Подскажите, пожалуйста, чем помочь. (тест деплоя)", voice_greeting_kz="Сәлеметсіз бе. Мен Айнурмын. Қалай көмектесе аламын?", ) 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())