Add sync_ai_operator_config_from_code() which overwrites the DB-cached ai_operator_settings row from ai_operator_default_config() on startup of ai_orchestrator_service and ai_voice_runtime_service. Greeting and system prompt changes now go through git + deploy instead of manual psql/API edits to prod. Also adds a root README pointing to existing docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
95 lines
4.4 KiB
Python
95 lines
4.4 KiB
Python
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())
|