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())